460 lines
18 KiB
Python
460 lines
18 KiB
Python
# Copyright (c) 2013-2014 Rackspace, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
# implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import base64
|
|
|
|
from oslo.config import cfg
|
|
from stevedore import named
|
|
|
|
from barbican.common import exception
|
|
from barbican.common import utils
|
|
from barbican.crypto import mime_types
|
|
from barbican.crypto import plugin as plugin_mod
|
|
from barbican.model import models
|
|
from barbican.openstack.common import gettextutils as u
|
|
|
|
|
|
CONF = cfg.CONF
|
|
DEFAULT_PLUGIN_NAMESPACE = 'barbican.crypto.plugin'
|
|
DEFAULT_PLUGINS = ['simple_crypto']
|
|
|
|
crypto_opt_group = cfg.OptGroup(name='crypto',
|
|
title='Crypto Plugin Options')
|
|
crypto_opts = [
|
|
cfg.StrOpt('namespace',
|
|
default=DEFAULT_PLUGIN_NAMESPACE,
|
|
help=u._('Extension namespace to search for plugins.')
|
|
),
|
|
cfg.MultiStrOpt('enabled_crypto_plugins',
|
|
default=DEFAULT_PLUGINS,
|
|
help=u._('List of crypto plugins to load.')
|
|
)
|
|
]
|
|
CONF.register_group(crypto_opt_group)
|
|
CONF.register_opts(crypto_opts, group=crypto_opt_group)
|
|
|
|
|
|
class CryptoContentTypeNotSupportedException(exception.BarbicanException):
|
|
"""Raised when support for payload content type is not available."""
|
|
def __init__(self, content_type):
|
|
super(CryptoContentTypeNotSupportedException, self).__init__(
|
|
u._("Crypto Content Type "
|
|
"of '{0}' not supported").format(content_type)
|
|
)
|
|
self.content_type = content_type
|
|
|
|
|
|
class CryptoContentEncodingNotSupportedException(exception.BarbicanException):
|
|
"""Raised when support for payload content encoding is not available."""
|
|
def __init__(self, content_encoding):
|
|
super(CryptoContentEncodingNotSupportedException, self).__init__(
|
|
u._("Crypto Content-Encoding of '{0}' not supported").format(
|
|
content_encoding)
|
|
)
|
|
self.content_encoding = content_encoding
|
|
|
|
|
|
class CryptoAcceptNotSupportedException(exception.BarbicanException):
|
|
"""Raised when requested decrypted content-type is not available."""
|
|
def __init__(self, accept):
|
|
super(CryptoAcceptNotSupportedException, self).__init__(
|
|
u._("Crypto Accept of '{0}' not supported").format(accept)
|
|
)
|
|
self.accept = accept
|
|
|
|
|
|
class CryptoAlgorithmNotSupportedException(exception.BarbicanException):
|
|
"""Raised when support for an algorithm is not available."""
|
|
def __init__(self, algorithm):
|
|
super(CryptoAlgorithmNotSupportedException, self).__init__(
|
|
u._("Crypto algorithm of '{0}' not supported").format(
|
|
algorithm)
|
|
)
|
|
self.algorithm = algorithm
|
|
|
|
|
|
class CryptoPayloadDecodingError(exception.BarbicanException):
|
|
"""Raised when payload could not be decoded."""
|
|
def __init__(self):
|
|
super(CryptoPayloadDecodingError, self).__init__(
|
|
u._("Problem decoding payload")
|
|
)
|
|
|
|
|
|
class CryptoSupportedPluginNotFound(exception.BarbicanException):
|
|
"""
|
|
Raised when no plugins are found that support the requested
|
|
operation.
|
|
"""
|
|
message = "Crypto plugin not found for requested operation."
|
|
|
|
|
|
class CryptoPluginNotFound(exception.BarbicanException):
|
|
"""Raised when no plugins are installed."""
|
|
message = u._("Crypto plugin not found.")
|
|
|
|
|
|
class CryptoNoPayloadProvidedException(exception.BarbicanException):
|
|
"""Raised when secret information is not provided."""
|
|
def __init__(self):
|
|
super(CryptoNoPayloadProvidedException, self).__init__(
|
|
u._('No secret information provided to encrypt.')
|
|
)
|
|
|
|
|
|
class CryptoNoSecretOrDataFoundException(exception.BarbicanException):
|
|
"""Raised when secret information could not be located."""
|
|
def __init__(self, secret_id):
|
|
super(CryptoNoSecretOrDataFoundException, self).__init__(
|
|
u._('No secret information located for '
|
|
'secret {0}').format(secret_id)
|
|
)
|
|
self.secret_id = secret_id
|
|
|
|
|
|
class CryptoContentEncodingMustBeBase64(exception.BarbicanException):
|
|
"""Raised when encoding must be base64."""
|
|
def __init__(self):
|
|
super(CryptoContentEncodingMustBeBase64, self).__init__(
|
|
u._("Encoding type must be 'base64' for text-based payloads.")
|
|
)
|
|
|
|
|
|
class CryptoKEKBindingException(exception.BarbicanException):
|
|
"""Raised when the bind_kek_metadata method from a plugin returns None."""
|
|
def __init__(self, plugin_name=u._('Unknown')):
|
|
super(CryptoKEKBindingException, self).__init__(
|
|
u._('Failed to bind kek metadata for '
|
|
'plugin: {0}').format(plugin_name)
|
|
)
|
|
self.plugin_name = plugin_name
|
|
|
|
|
|
class CryptoGeneralException(exception.BarbicanException):
|
|
"""Raised when a system fault has occurred."""
|
|
def __init__(self, reason=u._('Unknown')):
|
|
super(CryptoGeneralException, self).__init__(
|
|
u._('Problem seen during crypto processing - '
|
|
'Reason: {0}').format(reason)
|
|
)
|
|
self.reason = reason
|
|
|
|
|
|
def normalize_before_encryption(unencrypted, content_type, content_encoding,
|
|
enforce_text_only=False):
|
|
"""Normalize unencrypted prior to plugin encryption processing."""
|
|
if not unencrypted:
|
|
raise CryptoNoPayloadProvidedException()
|
|
|
|
# Validate and normalize content-type.
|
|
normalized_mime = mime_types.normalize_content_type(content_type)
|
|
if not mime_types.is_supported(normalized_mime):
|
|
raise CryptoContentTypeNotSupportedException(content_type)
|
|
|
|
# Process plain-text type.
|
|
if normalized_mime in mime_types.PLAIN_TEXT:
|
|
# normalize text to binary string
|
|
unencrypted = unencrypted.encode('utf-8')
|
|
|
|
# Process binary type.
|
|
else:
|
|
# payload has to be decoded
|
|
if mime_types.is_base64_processing_needed(content_type,
|
|
content_encoding):
|
|
try:
|
|
unencrypted = base64.b64decode(unencrypted)
|
|
except TypeError:
|
|
raise CryptoPayloadDecodingError()
|
|
elif enforce_text_only:
|
|
# For text-based protocols (such as the one-step secret POST),
|
|
# only 'base64' encoding is possible/supported.
|
|
raise CryptoContentEncodingMustBeBase64()
|
|
elif content_encoding:
|
|
# Unsupported content-encoding request.
|
|
raise CryptoContentEncodingNotSupportedException(content_encoding)
|
|
|
|
return unencrypted, normalized_mime
|
|
|
|
|
|
def analyze_before_decryption(content_type):
|
|
"""Determine support for desired content type."""
|
|
if not mime_types.is_supported(content_type):
|
|
raise CryptoAcceptNotSupportedException(content_type)
|
|
|
|
|
|
def denormalize_after_decryption(unencrypted, content_type):
|
|
"""Translate the decrypted data into the desired content type."""
|
|
# Process plain-text type.
|
|
if content_type in mime_types.PLAIN_TEXT:
|
|
# normalize text to binary string
|
|
try:
|
|
unencrypted = unencrypted.decode('utf-8')
|
|
except UnicodeDecodeError:
|
|
raise CryptoAcceptNotSupportedException(content_type)
|
|
|
|
# Process binary type.
|
|
elif content_type not in mime_types.BINARY:
|
|
raise CryptoGeneralException(
|
|
u._("Unexpected content-type: '{0}'").format(content_type))
|
|
|
|
return unencrypted
|
|
|
|
|
|
class CryptoExtensionManager(named.NamedExtensionManager):
|
|
def __init__(self, conf=CONF, invoke_on_load=True,
|
|
invoke_args=(), invoke_kwargs={}):
|
|
super(CryptoExtensionManager, self).__init__(
|
|
conf.crypto.namespace,
|
|
conf.crypto.enabled_crypto_plugins,
|
|
invoke_on_load=invoke_on_load,
|
|
invoke_args=invoke_args,
|
|
invoke_kwds=invoke_kwargs
|
|
)
|
|
|
|
def encrypt(self, unencrypted, content_type, content_encoding,
|
|
secret, tenant, kek_repo, enforce_text_only=False):
|
|
"""Delegates encryption to first plugin that supports it."""
|
|
|
|
if len(self.extensions) < 1:
|
|
raise CryptoPluginNotFound()
|
|
|
|
for ext in self.extensions:
|
|
if ext.obj.supports(plugin_mod.PluginSupportTypes.ENCRYPT_DECRYPT):
|
|
encrypting_plugin = ext.obj
|
|
break
|
|
else:
|
|
raise CryptoSupportedPluginNotFound()
|
|
|
|
unencrypted, content_type = normalize_before_encryption(
|
|
unencrypted, content_type, content_encoding,
|
|
enforce_text_only=enforce_text_only)
|
|
|
|
# Find or create a key encryption key metadata.
|
|
kek_datum, kek_meta_dto = self._find_or_create_kek_objects(
|
|
encrypting_plugin, tenant, kek_repo)
|
|
|
|
encrypt_dto = plugin_mod.EncryptDTO(unencrypted)
|
|
# Create an encrypted datum instance and add the encrypted cypher text.
|
|
datum = models.EncryptedDatum(secret, kek_datum)
|
|
datum.content_type = content_type
|
|
response_dto = encrypting_plugin.encrypt(
|
|
encrypt_dto, kek_meta_dto, tenant.keystone_id
|
|
)
|
|
|
|
datum.cypher_text = response_dto.cypher_text
|
|
datum.kek_meta_extended = response_dto.kek_meta_extended
|
|
|
|
# Convert binary data into a text-based format.
|
|
#TODO(jwood) Figure out by storing binary (BYTEA) data in Postgres
|
|
# isn't working.
|
|
datum.cypher_text = base64.b64encode(datum.cypher_text)
|
|
|
|
return datum
|
|
|
|
def decrypt(self, content_type, secret, tenant):
|
|
"""Delegates decryption to active plugins."""
|
|
|
|
if not secret or not secret.encrypted_data:
|
|
raise CryptoNoSecretOrDataFoundException(secret.id)
|
|
|
|
analyze_before_decryption(content_type)
|
|
|
|
for ext in self.extensions:
|
|
decrypting_plugin = ext.obj
|
|
for datum in secret.encrypted_data:
|
|
if self._plugin_supports(decrypting_plugin,
|
|
datum.kek_meta_tenant):
|
|
# wrap the KEKDatum instance in our DTO
|
|
kek_meta_dto = plugin_mod.KEKMetaDTO(datum.kek_meta_tenant)
|
|
|
|
# Convert from text-based storage format to binary.
|
|
#TODO(jwood) Figure out by storing binary (BYTEA) data in
|
|
# Postgres isn't working.
|
|
encrypted = base64.b64decode(datum.cypher_text)
|
|
decrypt_dto = plugin_mod.DecryptDTO(encrypted)
|
|
|
|
# Decrypt the secret.
|
|
unencrypted = decrypting_plugin \
|
|
.decrypt(decrypt_dto,
|
|
kek_meta_dto,
|
|
datum.kek_meta_extended,
|
|
tenant.keystone_id)
|
|
|
|
# Denormalize the decrypted info per request.
|
|
return denormalize_after_decryption(unencrypted,
|
|
content_type)
|
|
else:
|
|
raise CryptoPluginNotFound()
|
|
|
|
def generate_symmetric_encryption_key(self, secret, content_type, tenant,
|
|
kek_repo):
|
|
"""Delegates generating a key to the first supported plugin.
|
|
|
|
Note that this key can be used by clients for their encryption
|
|
processes. This generated key is then be encrypted via
|
|
the plug-in key encryption process, and that encrypted datum
|
|
is then returned from this method.
|
|
"""
|
|
encrypting_plugin = \
|
|
self._determine_crypto_plugin(secret.algorithm,
|
|
secret.bit_length,
|
|
secret.mode)
|
|
|
|
kek_datum, kek_meta_dto = self._find_or_create_kek_objects(
|
|
encrypting_plugin, tenant, kek_repo)
|
|
|
|
# Create an encrypted datum instance and add the created cypher text.
|
|
datum = models.EncryptedDatum(secret, kek_datum)
|
|
datum.content_type = content_type
|
|
|
|
generate_dto = plugin_mod.GenerateDTO(secret.algorithm,
|
|
secret.bit_length,
|
|
secret.mode, None)
|
|
# Create the encrypted meta.
|
|
response_dto = encrypting_plugin.generate_symmetric(generate_dto,
|
|
kek_meta_dto,
|
|
tenant.keystone_id)
|
|
|
|
# Convert binary data into a text-based format.
|
|
# TODO(jwood) Figure out by storing binary (BYTEA) data in Postgres
|
|
# isn't working.
|
|
datum.cypher_text = base64.b64encode(response_dto.cypher_text)
|
|
datum.kek_meta_extended = response_dto.kek_meta_extended
|
|
return datum
|
|
|
|
def generate_asymmetric_encryption_keys(self, meta, content_type, tenant,
|
|
kek_repo):
|
|
"""Delegates generating a asymmteric keys to the first
|
|
supported plugin based on `meta`. meta will provide extra
|
|
information to help key generation.
|
|
Based on passpharse in meta this method will return a tuple
|
|
with two/three objects.
|
|
|
|
Note that this key can be used by clients for their encryption
|
|
processes. This generated key is then be encrypted via
|
|
the plug-in key encryption process, and that encrypted datum
|
|
is then returned from this method.
|
|
"""
|
|
encrypting_plugin = \
|
|
self._determine_crypto_plugin(meta.algorithm,
|
|
meta.bit_length,
|
|
meta.passphrase)
|
|
|
|
kek_datum, kek_meta_dto = self._find_or_create_kek_objects(
|
|
encrypting_plugin, tenant, kek_repo)
|
|
|
|
generate_dto = plugin_mod.GenerateDTO(meta.algorithm,
|
|
meta.bit_length,
|
|
None, meta.passphrase)
|
|
# generate the secret.
|
|
private_key_dto, public_key_dto, passwd_dto = \
|
|
encrypting_plugin.generate_asymmetric(
|
|
generate_dto,
|
|
kek_meta_dto,
|
|
tenant.keystone_id)
|
|
|
|
# Create an encrypted datum instances for each secret type
|
|
# and add the created cypher text.
|
|
priv_datum = models.EncryptedDatum(None, kek_datum)
|
|
priv_datum.content_type = content_type
|
|
priv_datum.cypher_text = base64.b64encode(private_key_dto.cypher_text)
|
|
priv_datum.kek_meta_extended = private_key_dto.kek_meta_extended
|
|
|
|
public_datum = models.EncryptedDatum(None, kek_datum)
|
|
public_datum.content_type = content_type
|
|
public_datum.cypher_text = base64.b64encode(public_key_dto.cypher_text)
|
|
public_datum.kek_meta_extended = public_key_dto.kek_meta_extended
|
|
|
|
passwd_datum = None
|
|
if passwd_dto:
|
|
passwd_datum = models.EncryptedDatum(None, kek_datum)
|
|
passwd_datum.content_type = content_type
|
|
passwd_datum.cypher_text = base64.b64encode(passwd_dto.cypher_text)
|
|
passwd_datum.kek_meta_extended = \
|
|
passwd_dto.kek_meta_extended
|
|
|
|
return (priv_datum, public_datum, passwd_datum)
|
|
|
|
def _determine_type(self, algorithm):
|
|
"""Determines the type (symmetric and asymmetric for now)
|
|
based on algorithm"""
|
|
symmetric_algs = plugin_mod.PluginSupportTypes.SYMMETRIC_ALGORITHMS
|
|
asymmetric_algs = plugin_mod.PluginSupportTypes.ASYMMETRIC_ALGORITHMS
|
|
if algorithm.lower() in symmetric_algs:
|
|
return plugin_mod.PluginSupportTypes.SYMMETRIC_KEY_GENERATION
|
|
elif algorithm.lower() in asymmetric_algs:
|
|
return plugin_mod.PluginSupportTypes.ASYMMETRIC_KEY_GENERATION
|
|
else:
|
|
raise CryptoAlgorithmNotSupportedException(algorithm)
|
|
|
|
#TODO(atiwari): Use meta object instead of individual attribute
|
|
#This has to be done while integration rest resources
|
|
def _determine_crypto_plugin(self, algorithm, bit_length=None,
|
|
mode=None):
|
|
"""Determines the generation type and encrypting plug-in
|
|
which supports the generation of secret based on
|
|
generation type"""
|
|
if len(self.extensions) < 1:
|
|
raise CryptoPluginNotFound()
|
|
|
|
generation_type = self._determine_type(algorithm)
|
|
for ext in self.extensions:
|
|
if ext.obj.supports(generation_type, algorithm,
|
|
bit_length,
|
|
mode):
|
|
encrypting_plugin = ext.obj
|
|
break
|
|
else:
|
|
raise CryptoSupportedPluginNotFound()
|
|
|
|
return encrypting_plugin
|
|
|
|
def _plugin_supports(self, plugin_inst, kek_metadata_tenant):
|
|
"""Tests for plugin support.
|
|
|
|
Tests if the supplied plugin supports operations on the supplied
|
|
key encryption key (KEK) metadata.
|
|
|
|
:param plugin_inst: The plugin instance to test.
|
|
:param kek_metadata: The KEK metadata to test.
|
|
:return: True if the plugin can support operations on the KEK metadata.
|
|
|
|
"""
|
|
plugin_name = utils.generate_fullname_for(plugin_inst)
|
|
return plugin_name == kek_metadata_tenant.plugin_name
|
|
|
|
def _find_or_create_kek_objects(self, plugin_inst, tenant, kek_repo):
|
|
# Find or create a key encryption key.
|
|
full_plugin_name = utils.generate_fullname_for(plugin_inst)
|
|
kek_datum = kek_repo.find_or_create_kek_datum(tenant,
|
|
full_plugin_name)
|
|
|
|
# Bind to the plugin's key management.
|
|
# TODO(jwood): Does this need to be in a critical section? Should the
|
|
# bind operation just be declared idempotent in the plugin contract?
|
|
kek_meta_dto = plugin_mod.KEKMetaDTO(kek_datum)
|
|
if not kek_datum.bind_completed:
|
|
kek_meta_dto = plugin_inst.bind_kek_metadata(kek_meta_dto)
|
|
|
|
# By contract, enforce that plugins return a
|
|
# (typically modified) DTO.
|
|
if kek_meta_dto is None:
|
|
raise CryptoKEKBindingException(full_plugin_name)
|
|
|
|
plugin_mod.indicate_bind_completed(kek_meta_dto, kek_datum)
|
|
kek_repo.save(kek_datum)
|
|
|
|
return kek_datum, kek_meta_dto
|