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 67bf5fa..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 @@ -20,7 +21,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 +30,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. @@ -67,7 +72,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 @@ -77,12 +82,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): @@ -118,31 +133,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, @@ -153,10 +183,18 @@ 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) + 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. @@ -333,9 +371,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 @@ -346,7 +384,9 @@ def call(self, endpoint, method='POST', params=None, data=None): success. """ endpoint = '%s/%s' % (self.uri_base, endpoint) - if data: + if data is None: + data = {} + if 'Name' not in data: data.update({ 'Name': self.name, 'Password': self.password, @@ -355,20 +395,15 @@ def call(self, endpoint, method='POST', params=None, data=None): method=method, url=endpoint, params=params, - data=data, + json=data, + verify=self.verify, ) 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. diff --git a/red_october/tests/test_red_october.py b/red_october/tests/test_red_october.py index aa3be7d..5f83453 100644 --- a/red_october/tests/test_red_october.py +++ b/red_october/tests/test_red_october.py @@ -196,7 +196,8 @@ def test_call_request(self, requests): method='method', url='https://test:1/endpoint', params='params', - data=data, + json=data, + verify=True, ) @mock.patch.object(requests, 'request')