From 62723aa23c23830d6daef0c3da2ae43ae42050ad Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Sat, 24 Dec 2016 15:26:36 -0800 Subject: [PATCH 1/4] [IMP] Add optional certificate verification --- red_october/red_october.py | 7 ++++++- red_october/tests/test_red_october.py | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/red_october/red_october.py b/red_october/red_october.py index 67bf5fa..c4bdc34 100644 --- a/red_october/red_october.py +++ b/red_october/red_october.py @@ -20,7 +20,7 @@ class RedOctober(object): https://github.com/cloudflare/redoctober """ - def __init__(self, host, port, name, password, ssl=True): + def __init__(self, host, port, name, password, ssl=True, verify=True): """ It initializes the RedOctober API using the provided credentials. Args: @@ -29,11 +29,15 @@ def __init__(self, host, port, name, password, ssl=True): name (str): Account name for use as login. password (str): Password for account. ssl (bool): Is server SSL encrypted? + verify (bool or str): File path of CA cert for verification, + `True` to use system certs, or `False` to disable certificate + verification. """ ssl = 'https' if ssl else 'http' self.uri_base = '%s://%s:%d' % (ssl, host, port) self.name = name self.password = password + self.verify = verify def create_vault(self): """ It creates a new vault. @@ -356,6 +360,7 @@ def call(self, endpoint, method='POST', params=None, data=None): url=endpoint, params=params, data=data, + verify=self.verify, ) response = response.json() if response['Status'] != 'ok': diff --git a/red_october/tests/test_red_october.py b/red_october/tests/test_red_october.py index aa3be7d..685cfaf 100644 --- a/red_october/tests/test_red_october.py +++ b/red_october/tests/test_red_october.py @@ -197,6 +197,7 @@ def test_call_request(self, requests): url='https://test:1/endpoint', params='params', data=data, + verify=True, ) @mock.patch.object(requests, 'request') From 99acfacf9273a45fe5fd30a399234398b72ef67f Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Sat, 24 Dec 2016 15:53:44 -0800 Subject: [PATCH 2/4] [FIX] Send data via JSON in `call` --- red_october/red_october.py | 17 +++++++++-------- red_october/tests/test_red_october.py | 2 +- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/red_october/red_october.py b/red_october/red_october.py index c4bdc34..d9bf081 100644 --- a/red_october/red_october.py +++ b/red_october/red_october.py @@ -337,9 +337,9 @@ def call(self, endpoint, method='POST', params=None, data=None): Args: endpoint (str): RedOctober endpoint to call (e.g. ``newcert``). method (str): HTTP method to utilize for the Request. - params: (dict|bytes) Data to be sent in the query string + params: (dict or bytes) Data to be sent in the query string for the Request. - data: (dict|bytes|file) Data to send in the body of the + data: (dict or bytes or file) Data to send in the body of the Request. Raises: RedOctoberRemoteException: In the event of a ``False`` in the @@ -350,16 +350,17 @@ def call(self, endpoint, method='POST', params=None, data=None): success. """ endpoint = '%s/%s' % (self.uri_base, endpoint) - if data: - data.update({ - 'Name': self.name, - 'Password': self.password, - }) + if data is None: + data = {} + data.update({ + 'Name': self.name, + 'Password': self.password, + }) response = requests.request( method=method, url=endpoint, params=params, - data=data, + json=data, verify=self.verify, ) response = response.json() diff --git a/red_october/tests/test_red_october.py b/red_october/tests/test_red_october.py index 685cfaf..5f83453 100644 --- a/red_october/tests/test_red_october.py +++ b/red_october/tests/test_red_october.py @@ -196,7 +196,7 @@ def test_call_request(self, requests): method='method', url='https://test:1/endpoint', params='params', - data=data, + json=data, verify=True, ) From c8e9296161ee6aa0c57d1a669225927b1205e492 Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Sat, 24 Dec 2016 17:00:21 -0800 Subject: [PATCH 3/4] [IMP] Add a few features * Encrypt and decrypt as another user * Create another user --- red_october/red_october.py | 53 ++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/red_october/red_october.py b/red_october/red_october.py index d9bf081..5b2e947 100644 --- a/red_october/red_october.py +++ b/red_october/red_october.py @@ -71,7 +71,7 @@ def delegate(self, time=None, uses=None): }) return self.call('delegate', data=data) - def create_user(self, user_type='rsa'): + def create_user(self, user_type='rsa', name=None, password=None): """ It creates a new user account. Allows an optional ``UserType`` to be specified which controls how the @@ -81,12 +81,22 @@ def create_user(self, user_type='rsa'): Args: user_type (str): Controls how the record is encrypted. This can have a value of either ``ecc`` or ``rsa``. + name (str, optional): Account name for use as login. Omit to use + current session credentials. + password (str, optional): Password for account. Omit to use + current session credentials. Returns: bool: Status of user creation. """ + data = self._clean_mapping({ 'UserType': EnumUserType[user_type].name.upper(), }) + if name and password: + data.update({ + 'Name': name, + 'Password': password, + }) return self.call('create-user', data=data) def get_summary(self): @@ -122,31 +132,46 @@ def get_summary(self): """ return self.call('summary') - def encrypt(self, minimum, owners, data): + def encrypt(self, data, owners=None, minimum=1, name=None, password=None): """ It allows a user to encrypt a piece of data. Args: - minimum (int): Minimum number of users from ``owners`` set that - must have delegated their keys to the server. - owners (iter): Iterator of strings indicating users that may - decrypt the document. data (str): Data to encrypt. + owners (iter of str, optional): Representing the users that may + decrypt the document. None for self-owned. + minimum (int, optional): Minimum number of users from ``owners`` + set that must have delegated their keys to the server. + name (str, optional): Account name for use as login. Omit to use + current session credentials. + password (str, optional): Password for account. Omit to use + current session credentials. Returns: str: Base64 encoded string representing the encrypted string. """ + if owners is None: + owners = [self.name] data = self._clean_mapping({ 'Minimum': minimum, 'Owners': owners, 'Data': data.encode('base64'), }) + if name and password: + data.update({ + 'Name': name, + 'Password': password, + }) return self.call('encrypt', data=data) - def decrypt(self, data): + def decrypt(self, data, name=None, password=None): """ It allows a user to decrypt a piece of data. Args: data (str): Base64 encoded string representing the encrypted string. + name (str, optional): Account name for use as login. Omit to use + current session credentials. + password (str, optional): Password for account. Omit to use + current session credentials. Raises: RedOctoberDecryptException: If not enough ``minimum`` users from the set of ``owners`` have delegated their keys to the server, @@ -157,6 +182,11 @@ def decrypt(self, data): data = self._clean_mapping({ 'Data': data, }) + if name and password: + data.update({ + 'Name': name, + 'Password': password, + }) try: return self.call('decrypt', data=data) except RedOctoberRemoteException as e: @@ -352,10 +382,11 @@ def call(self, endpoint, method='POST', params=None, data=None): endpoint = '%s/%s' % (self.uri_base, endpoint) if data is None: data = {} - data.update({ - 'Name': self.name, - 'Password': self.password, - }) + if 'Name' not in data: + data.update({ + 'Name': self.name, + 'Password': self.password, + }) response = requests.request( method=method, url=endpoint, From 02b0b37494d69c5713718a1b0358aa356df08fca Mon Sep 17 00:00:00 2001 From: Dave Lasley Date: Sat, 24 Dec 2016 17:00:38 -0800 Subject: [PATCH 4/4] [IMP] Add code samples to ReadMe --- README.rst | 100 +++++++++++++++++++++++++++++++++++++ red_october/red_october.py | 16 +++--- 2 files changed, 107 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index c9289c9..21912b7 100644 --- a/README.rst +++ b/README.rst @@ -41,6 +41,106 @@ Usage * `Read The API Documentation `_ +Connect to Red October +---------------------- + +Connect to a Red October instance ``https://172.17.0.2:8080`` as ``Admin`` with the password ``password``: + +.. code-block:: python + + from red_october import RedOctober + ro = RedOctober('172.17.0.2', 8080, 'Admin', 'password') + +Turn off certificate verification: + +.. code-block:: python + + from red_october import RedOctober + ro = RedOctober('172.17.0.2', 8080, 'Admin', 'password', verify=False) + +Create a Vault +-------------- + +This will create a vault using the currently logged in credentials as the first admin: + +.. code-block:: python + + ro.create_vault() + +Create a User +------------- + +If you would like to create a user for the currently logged in session: + +.. code-block:: python + + ro.create_user('rsa') + +If you are currently playing the role of an administrator, you can also create a user for someone else: + +.. code-block:: python + + ro.create_user('ecc', name="Ashley", password="@shl3y!") + +Delegate Decryption Rights +-------------------------- + +In order to decrypt a piece of data, the Vault will need to be delegated decryption rights +by document owners. + +In order to delegate rights to a Vault, and subsequently grant access to your data: + +.. code-block:: python + + from datetime import timedelta + delegate_time = timedelta(days=1) + delegate_uses = 20 + ro.delegate(delegate_time, delegate_uses) + +The above delegation will expire in 1 day or 20 uses, whichever comes first. + +Encrypt Some Data +----------------- + +To encrypt some data so that only you can decrypt it: + +.. code-block:: python + + data = 'Super Secret Stuff!' + encrypted = ro.encrypt(data) + +To encrypt some data with multiple owners, also setting a minimum amount of delegations +that are required for decryption: + +.. code-block:: python + + owners = ['Admin', 'Ashley', 'Bob', 'Jenna'] + minimum_delegations = 2 + data = 'Super Secret Stuff!' + encrypted = ro.encrypt(data, owners, minimum_delegations) + +To encrypt data as another user: + +.. code-block:: python + + data = 'Super Secret Stuff!' + encrypted_string = ro.encrypt(data, name='Todd', password='Todd Pass') + +Decrypt Some Data +----------------- + +Decryption will use the current session credentials by default: + +.. code-block:: python + + decrypted_string = ro.decrypt(encrypted_string) + +To decrypt as another user: + +.. code-block:: python + + decrypted_string = ro.decrypt(encrypted_string, name='Todd', password='Todd Pass') + Known Issues / Road Map ======================= diff --git a/red_october/red_october.py b/red_october/red_october.py index 5b2e947..70b4572 100644 --- a/red_october/red_october.py +++ b/red_october/red_october.py @@ -2,6 +2,7 @@ # Copyright 2016 LasLabs Inc. # License MIT (https://opensource.org/licenses/MIT). +import json import requests from datetime import timedelta @@ -188,9 +189,12 @@ def decrypt(self, data, name=None, password=None): 'Password': password, }) try: - return self.call('decrypt', data=data) + data = self.call('decrypt', data=data) except RedOctoberRemoteException as e: raise RedOctoberDecryptException(e.message) + data = json.loads(data.decode('base64')) + return data['Data'] + def get_owners(self, data): """ It provides the delegates required to decrypt a piece of data. @@ -397,15 +401,9 @@ def call(self, endpoint, method='POST', params=None, data=None): response = response.json() if response['Status'] != 'ok': raise RedOctoberRemoteException( - '\n'.join([ - 'Response:', - '\n'.join(response.get('Response', [])), - ]) + response['Status'], ) - try: - return response['Response'] - except KeyError: - return True + return response.get('Response', True) def _clean_mapping(self, mapping): """ It removes false entries from mapping.