def test_round_trip_json(self): as_json = self.payload.json() payload = EncryptedPayload.from_json(as_json) self.assertEquals(IV, payload.iv) self.assertEquals(CIPHERTEXT, payload.ciphertext) self.assertEquals(KEY, payload.key) self.assertEquals(ORBIT_REGION, payload.key_region) self.assertEquals(ENCODING, payload.encoding)
def test_round_trip_dynamodb(self): dynamodb_item = self.payload.dynamodb_item() payload = EncryptedPayload.from_dynamodb_item(dynamodb_item) self.assertEquals(IV, payload.iv) self.assertEquals(CIPHERTEXT, payload.ciphertext) self.assertEquals(KEY, payload.key) self.assertEquals(ORBIT_REGION, payload.key_region) self.assertEquals(ENCODING, payload.encoding)
def test_set_password_existing(self): self.dynamodb.get_item.return_value = { 'Item': (EncryptedPayload('a', 'b', 'c', ORBIT_REGION, 'utf-8').dynamodb_item()) } was_set = self.password.set_password(self.app_region, PASSWORD_NAME, lambda: PASSWORD) self.assertFalse(was_set) self.dynamodb.put_item.assert_not_called()
def test_from_json(self): payload = EncryptedPayload.from_json('''{ "ciphertext": "MDAwMDAwMDAwMDAwMDAwMA==", "encoding": "utf-8", "iv": "MDAwMDAwMDAwMDAwMDAwMA==", "key": "MDAwMDAwMDAwMDAwMDAwMA==", "key_region": "us-east-1" }''') self.assertTestPayload(payload)
def get_password(self, app_region, label, length=64, generate=True): """ Get a password for an application in a region. :param app_region: Application configuration in region. :param label: Password label. :param length: Password length. :param generate: Generate new password if missing. :return: Encrypted password. """ app_name = app_region.app.name region = app_region.region logger.debug('Getting password for %s in %s.', label, app_name) table_name = '%s-passwords' % app_region.app.orbit.name password_name = '%s:%s' % (app_name, label) dynamodb = self._clients.dynamodb(region) existing_item = dynamodb.get_item(TableName=table_name, Key={ 'name': { 'S': password_name } }).get('Item') if existing_item: logger.debug('Found existing password for %s in %s.', label, app_name) encrypted = EncryptedPayload.from_dynamodb_item(existing_item) def decrypt_func(): """Decrypt via KMS.""" return self._kms_crypt.decrypt_payload(encrypted) return encrypted, decrypt_func if not generate: return None, lambda: None # Not found, generate: logger.debug('Generating password for %s in %s.', label, app_name) plaintext = self._generate_password(length) encrypted_payload = self._kms_crypt.encrypt(app_region, plaintext) password_item = encrypted_payload.dynamodb_item() password_item['name'] = {'S': password_name} # Persist encrypted password: dynamodb.put_item( TableName=table_name, Item=password_item, ConditionExpression='attribute_not_exists(ciphertext)') # Plaintext can be returned _once_ without Decrypt() permission return encrypted_payload, lambda: plaintext
def get_password(self, app_region, label, length=64, generate=True): """ Get a password for an application in a region. :param app_region: Application configuration in region. :param label: Password label. :param length: Password length. :param generate: Generate new password if missing. :return: Encrypted password. """ app_name = app_region.app.name region = app_region.region logger.debug('Getting password for %s in %s.', label, app_name) table_name = '%s-passwords' % app_region.app.orbit.name password_name = '%s:%s' % (app_name, label) dynamodb = self._clients.dynamodb(region) existing_item = dynamodb.get_item( TableName=table_name, Key={'name': {'S': password_name}} ).get('Item') if existing_item: logger.debug('Found existing password for %s in %s.', label, app_name) encrypted = EncryptedPayload.from_dynamodb_item(existing_item) def decrypt_func(): """Decrypt via KMS.""" return self._kms_crypt.decrypt_payload(encrypted) return encrypted, decrypt_func if not generate: return None, lambda: None # Not found, generate: logger.debug('Generating password for %s in %s.', label, app_name) plaintext = self._generate_password(length) encrypted_payload = self._kms_crypt.encrypt(app_region, plaintext) password_item = encrypted_payload.dynamodb_item() password_item['name'] = {'S': password_name} # Persist encrypted password: dynamodb.put_item( TableName=table_name, Item=password_item, ConditionExpression='attribute_not_exists(ciphertext)') # Plaintext can be returned _once_ without Decrypt() permission return encrypted_payload, lambda: plaintext
def encrypt(self, app_region, plaintext, create_key=True): """ Encrypt data for an application. :param app_region: SpaceAppRegion. :param plaintext: Plaintext blob. :param create_key: Create key if missing (else fail). :return: EncryptedPayload. """ region = app_region.region # Get DEK: logger.debug('Fetching fresh data key...') try: alias_name = self._kms_key.get_key_alias(app_region) kms = self._clients.kms(region) data_key = kms.generate_data_key(KeyId=alias_name, KeySpec='AES_256') except ClientError as e: e_message = e.response['Error'].get('Message', '') if create_key and 'is not found' in e_message: # Key not found, create and try again: self._kms_key.create_key(app_region) return self.encrypt(app_region, plaintext) raise e # Encode and pad data: encoding = 'bytes' if isinstance(plaintext, six.string_types): encoding = 'utf-8' if six.PY3: # pragma: no cover plaintext = bytes(plaintext, encoding) else: # pragma: no cover plaintext = plaintext.encode(encoding) pad_length = BLOCK_SIZE - (len(plaintext) % BLOCK_SIZE) padded = plaintext + (pad_length * bchr(pad_length)) logger.debug('Padded %s %s to %s.', len(plaintext), encoding, len(padded)) logger.debug('Encrypting data with data key...') iv = self._random.read(BLOCK_SIZE) cipher = AES.new(data_key['Plaintext'], CIPHER_MODE, iv) ciphertext = cipher.encrypt(padded) encrypted_key = data_key['CiphertextBlob'] return EncryptedPayload(iv, ciphertext, encrypted_key, region, encoding)
def test_get_password_existing(self): self.dynamodb.get_item.return_value = { 'Item': (EncryptedPayload('a', 'b', 'c', ORBIT_REGION, 'utf-8').dynamodb_item()) } encrypted, decrypt_func = self.password.get_password( self.app_region, PASSWORD_NAME) self.assertIsInstance(encrypted, EncryptedPayload) self.assertEquals(encrypted.iv, 'a') self.dynamodb.put_item.assert_not_called() self.kms_crypto.encrypt.assert_not_called() # Decrypt is deferred until absolutely necessary: self.kms_crypto.decrypt_payload.assert_not_called() decrypt_func() self.kms_crypto.decrypt_payload.assert_called_with(ANY)
def test_from_json_invalid_json(self): payload = EncryptedPayload.from_json('{}') self.assertIsNone(payload)
def test_from_json_not_json(self): payload = EncryptedPayload.from_json('meow') self.assertIsNone(payload)
import unittest from spacel.security.payload import EncryptedPayload from test import ORBIT_REGION IV = b'0000000000000000' CIPHERTEXT = b'0000000000000000' KEY = b'0000000000000000' ENCODING = 'utf-8' ENCRYPTED_PAYLOAD = EncryptedPayload(IV, CIPHERTEXT, KEY, ORBIT_REGION, ENCODING) class TestEncryptedPayload(unittest.TestCase): def setUp(self): self.payload = ENCRYPTED_PAYLOAD def test_round_trip_dynamodb(self): dynamodb_item = self.payload.dynamodb_item() payload = EncryptedPayload.from_dynamodb_item(dynamodb_item) self.assertEquals(IV, payload.iv) self.assertEquals(CIPHERTEXT, payload.ciphertext) self.assertEquals(KEY, payload.key) self.assertEquals(ORBIT_REGION, payload.key_region) self.assertEquals(ENCODING, payload.encoding) def test_round_trip_json(self): as_json = self.payload.json() payload = EncryptedPayload.from_json(as_json) self.assertEquals(IV, payload.iv) self.assertEquals(CIPHERTEXT, payload.ciphertext)
from spacel.security.payload import EncryptedPayload IV = b'0000000000000000' CIPHERTEXT = b'0000000000000000' KEY = b'0000000000000000' ENCODING = 'utf-8' REGION = 'us-east-1' PAYLOAD = EncryptedPayload(IV, CIPHERTEXT, KEY, REGION, ENCODING)
def test_json_round_trip(self): as_json = PAYLOAD.json() self.assertIsInstance(as_json, str) from_json = EncryptedPayload.from_json(as_json) self.assertTestPayload(from_json)