def delete(self, deletes, force=False): _deletes = ', '.join([delete.id for delete in deletes]) if not force: logger.info( 'Would have deleted credential and revisions: {}'.format( _deletes, )) return logger.info('Deleting credential and revisions: {}'.format(_deletes, )) with Credential.batch_write() as batch: for delete in deletes: batch.delete(delete) stats.incr('archive.delete.success')
def save(self, saves, force=False): _saves = ', '.join([save.id for save in saves]) if not force: logger.info( 'Would have archived credential and revisions: {}'.format( _saves, )) return logger.info('Archiving credential and revisions: {}'.format(_saves, )) with CredentialArchive.batch_write() as batch: for save in saves: batch.save(save) stats.incr('archive.save.success')
def create_datakey(encryption_context): ''' Create a datakey from KMS. ''' # Disabled encryption is dangerous, so we don't use falsiness here. if app.config['USE_ENCRYPTION'] is False: logging.warning('Creating a mock datakey in keymanager.create_datakey.' ' If you are not running in a development or test' ' environment, this should not be happening!') return cryptolib.create_mock_datakey() # underlying lib does generate random and encrypt, so increment by 2 stats.incr('at_rest_action', 2) return cryptolib.create_datakey( encryption_context, 'alias/{0}'.format(app.config.get('KMS_MASTER_KEY')))
def create_datakey(encryption_context): ''' Create a datakey from KMS. ''' # Disabled encryption is dangerous, so we don't use falsiness here. if app.config['USE_ENCRYPTION'] is False: logging.warning('Creating a mock datakey in keymanager.create_datakey.' ' If you are not running in a development or test' ' environment, this should not be happening!') return cryptolib.create_mock_datakey() # underlying lib does generate random and encrypt, so increment by 2 stats.incr('at_rest_action', 2) return cryptolib.create_datakey( encryption_context, 'alias/{0}'.format(app.config.get('KMS_MASTER_KEY')) )
def decrypt_datakey(data_key, encryption_context=None): ''' Decrypt a datakey. ''' # Disabled encryption is dangerous, so we don't use falsiness here. if app.config['USE_ENCRYPTION'] is False: logging.warning('Decypting a mock data key in' ' keymanager.decrypt_datakey. If you are not running' ' in a development or test environment, this should' ' not be happening!') return cryptolib.decrypt_mock_datakey(data_key) sha = hashlib.sha256(data_key).hexdigest() if sha not in DATAKEYS: stats.incr('at_rest_action') plaintext = cryptolib.decrypt_datakey(data_key, encryption_context) DATAKEYS[sha] = plaintext return DATAKEYS[sha]
def create_datakey(encryption_context): ''' Create a datakey from KMS. ''' at_rest_kms_client = _get_at_rest_kms_client() # Disabled encryption is dangerous, so we don't use falsiness here. if settings.USE_ENCRYPTION is False: logger.warning( 'Creating a mock datakey in keymanager.create_datakey. If you are' ' not running in a development or test environment, this should not' ' be happening!') return cryptolib.create_mock_datakey() # underlying lib does generate random and encrypt, so increment by 2 stats.incr('at_rest_action', 2) return cryptolib.create_datakey(encryption_context, settings.KMS_MASTER_KEY, client=at_rest_kms_client)
def issue_certificate_with_key(self, cn, validity, san=None): """ Given the string common name, the validity length of the certificate (in number of days), and a list of subject alternative names, return a dict with the PEM encoded certificate, certificate chain, and private RSA key. """ with stats.timer('issue_certificate_with_key'): cache_id = self.cache.get_cache_id(cn, validity, san) cached_response = self._get_cached_certificate_with_key(cache_id) if cached_response: stats.incr('get_cached_certificate_with_key.hit') logging.debug('Used cached response for {}'.format(cache_id)) return cached_response stats.incr('get_cached_certificate_with_key.miss') key = self.generate_key() encoded_key = self.encode_key(key) if self.settings['self_sign']: cert = self.encode_certificate( self.generate_self_signed_certificate( key, cn, validity, san, )) return { 'certificate': cert, 'certificate_chain': cert, 'key': encoded_key, } csr = self.generate_csr(key, cn, san) try: # set a lock self.cache.lock(cache_id) arn = self.issue_certificate(self.encode_csr(csr), validity) response = self.get_certificate_from_arn(arn) response['key'] = encoded_key self.cache.set_response(cache_id, response) finally: # release the lock self.cache.release(cache_id) return response
def decrypt_datakey(data_key, encryption_context=None): ''' Decrypt a datakey. ''' at_rest_kms_client = _get_at_rest_kms_client() # Disabled encryption is dangerous, so we don't use falsiness here. if settings.USE_ENCRYPTION is False: logger.warning( 'Decrypting a mock data key in keymanager.decrypt_datakey. If you' ' are not running in a development or test environment, this should' ' not be happening!') return cryptolib.decrypt_mock_datakey(data_key) sha = hashlib.sha256(data_key).hexdigest() if sha not in _DATAKEYS: stats.incr('at_rest_action') plaintext = cryptolib.decrypt_datakey(data_key, encryption_context, client=at_rest_kms_client) _DATAKEYS[sha] = plaintext return _DATAKEYS[sha]
def archive(self, credentials, force): services = list(Service.data_type_date_index.query('service')) for credential in credentials: if self.credential_in_service(credential.id, services): msg = ('Skipping archival of disabled credential {}, as it' ' is still mapped to a service.') logger.warning(msg.format(credential.id)) continue saves = [] deletes = [] # save the current record. archive_credential = CredentialArchive.from_credential( credential, ) saves.append(archive_credential) # fetch and save every revision revisions = Credential.batch_get( credentialmanager.get_revision_ids_for_credential(credential)) for revision in revisions: archive_revision = CredentialArchive.from_credential( revision, ) saves.append(archive_revision) deletes.append(revision) deletes.append(credential) try: self.save(saves, force=force) except Exception: logger.exception( 'Failed to batch save {}, skipping deletion.'.format( credential.id)) stats.incr('archive.save.failure') continue try: self.delete(deletes, force=force) except Exception: logger.exception('Failed to batch delete {}'.format( credential.id)) stats.incr('archive.delete.failure')
def restore(self, archive_credentials, force): for archive_credential in archive_credentials: saves = [] # restore the current record credential = Credential.from_archive_credential( archive_credential, ) saves.append(credential) # fetch and restore every revision _range = range(1, credential.revision + 1) ids = [] for i in _range: ids.append("{0}-{1}".format(credential.id, i)) archive_revisions = CredentialArchive.batch_get(ids) for archive_revision in archive_revisions: revision = Credential.from_archive_credential( archive_revision, ) saves.append(revision) try: self.save(saves, force=force) except Exception: logger.exception('Failed to batch save {}.'.format( credential.id)) stats.incr('restore.save.failure') continue
def save(self, saves, force=False): # Do not restore a credential if it exists in the primary table. # We do this check at the point of all saves so that we can # restore revisions, if one of them failed to restore for some # reason. _saves = [] for save in saves: if self.credential_exists(save.id): continue _saves.append(save) if not _saves: return save_msg = ', '.join([save.id for save in _saves]) if not force: logger.info( 'Would have restored credential and revisions: {}'.format( save_msg, )) return logger.info('Restoring credential and revisions: {}'.format( save_msg, )) with Credential.batch_write() as batch: for save in _saves: batch.save(save) stats.incr('restore.save.success')
def decrypt_token(version, user_type, _from, token): ''' Decrypt a token. ''' if (version > app.config['KMS_MAXIMUM_TOKEN_VERSION'] or version < app.config['KMS_MINIMUM_TOKEN_VERSION']): raise TokenDecryptionError('Unacceptable token version.') stats.incr('token_version_{0}'.format(version)) try: token_key = '{0}{1}'.format(hashlib.sha256(token).hexdigest(), _from) except Exception: raise TokenDecryptionError('Authentication error.') if token_key not in TOKENS: try: token = base64.b64decode(token) context = { # This key is sent to us. 'to': app.config['AUTH_CONTEXT'], # From a service. 'from': _from } if version > 1: context['user_type'] = user_type with stats.timer('kms_decrypt_token'): data = kms_client.decrypt(CiphertextBlob=token, EncryptionContext=context) # Decrypt doesn't take KeyId as an argument. We need to verify the # correct key was used to do the decryption. # Annoyingly, the KeyId from the data is actually an arn. key_arn = data['KeyId'] if user_type == 'service': if not valid_service_auth_key(key_arn): raise TokenDecryptionError( 'Authentication error (wrong KMS key).') elif user_type == 'user': if key_arn != get_key_arn(app.config['USER_AUTH_KEY']): raise TokenDecryptionError( 'Authentication error (wrong KMS key).') else: raise TokenDecryptionError( 'Authentication error. Unsupported user_type.') plaintext = data['Plaintext'] payload = json.loads(plaintext) key_alias = get_key_alias_from_cache(key_arn) ret = {'payload': payload, 'key_alias': key_alias} except TokenDecryptionError: raise # We don't care what exception is thrown. For paranoia's sake, fail # here. except Exception: logging.exception('Failed to validate token.') raise TokenDecryptionError('Authentication error. General error.') else: ret = TOKENS[token_key] time_format = "%Y%m%dT%H%M%SZ" now = datetime.datetime.utcnow() try: not_before = datetime.datetime.strptime(ret['payload']['not_before'], time_format) not_after = datetime.datetime.strptime(ret['payload']['not_after'], time_format) except Exception: logging.exception( 'Failed to get not_before and not_after from token payload.') raise TokenDecryptionError('Authentication error. Missing validity.') delta = (not_after - not_before).seconds / 60 if delta > app.config['AUTH_TOKEN_MAX_LIFETIME']: logging.warning('Token used which exceeds max token lifetime.') raise TokenDecryptionError( 'Authentication error. Token lifetime exceeded.') if (now < not_before) or (now > not_after): logging.warning('Invalid time validity for token.') raise TokenDecryptionError( 'Authentication error. Invalid time validity for token.') TOKENS[token_key] = ret return TOKENS[token_key]
def decrypt_token(version, user_type, _from, token): ''' Decrypt a token. ''' if (version > app.config['KMS_MAXIMUM_TOKEN_VERSION'] or version < app.config['KMS_MINIMUM_TOKEN_VERSION']): raise TokenDecryptionError('Unacceptable token version.') stats.incr('token_version_{0}'.format(version)) try: token_key = '{0}{1}'.format( hashlib.sha256(token).hexdigest(), _from ) except Exception: raise TokenDecryptionError('Authentication error.') if token_key not in TOKENS: try: token = base64.b64decode(token) context = { # This key is sent to us. 'to': app.config['AUTH_CONTEXT'], # From a service. 'from': _from } if version > 1: context['user_type'] = user_type with stats.timer('kms_decrypt_token'): data = kms_client.decrypt( CiphertextBlob=token, EncryptionContext=context ) # Decrypt doesn't take KeyId as an argument. We need to verify the # correct key was used to do the decryption. # Annoyingly, the KeyId from the data is actually an arn. key_arn = data['KeyId'] if user_type == 'service': if not valid_service_auth_key(key_arn): raise TokenDecryptionError( 'Authentication error (wrong KMS key).' ) elif user_type == 'user': if key_arn != get_key_arn(app.config['USER_AUTH_KEY']): raise TokenDecryptionError( 'Authentication error (wrong KMS key).' ) else: raise TokenDecryptionError( 'Authentication error. Unsupported user_type.' ) plaintext = data['Plaintext'] payload = json.loads(plaintext) key_alias = get_key_alias_from_cache(key_arn) ret = {'payload': payload, 'key_alias': key_alias} except TokenDecryptionError: raise # We don't care what exception is thrown. For paranoia's sake, fail # here. except Exception: logging.exception('Failed to validate token.') raise TokenDecryptionError('Authentication error. General error.') else: ret = TOKENS[token_key] time_format = "%Y%m%dT%H%M%SZ" now = datetime.datetime.utcnow() try: not_before = datetime.datetime.strptime( ret['payload']['not_before'], time_format ) not_after = datetime.datetime.strptime( ret['payload']['not_after'], time_format ) except Exception: logging.exception( 'Failed to get not_before and not_after from token payload.' ) raise TokenDecryptionError('Authentication error. Missing validity.') delta = (not_after - not_before).seconds / 60 if delta > app.config['AUTH_TOKEN_MAX_LIFETIME']: logging.warning('Token used which exceeds max token lifetime.') raise TokenDecryptionError( 'Authentication error. Token lifetime exceeded.' ) if (now < not_before) or (now > not_after): logging.warning('Invalid time validity for token.') raise TokenDecryptionError( 'Authentication error. Invalid time validity for token.' ) TOKENS[token_key] = ret return TOKENS[token_key]