Add a new crypt method using cryptography

This updates the default crypt method to use the cryptography module
instead of the oslo crypto utils module. It also refactors decrypt to
remove some duplication.

Change-Id: Ie24aebcb3080725c250a4f3ba726b23a9c995965
Closes-Bug: #1468025
This commit is contained in:
Thomas Herve 2015-06-24 11:50:39 +02:00
parent e6ecedca1a
commit d8f3ef910c
7 changed files with 72 additions and 80 deletions

View File

@ -12,13 +12,19 @@
# under the License.
import base64
import sys
from Crypto.Cipher import AES
from cryptography import fernet
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import encodeutils
from heat.common.i18n import _
from heat.common.i18n import _LW
from heat.openstack.common.crypto import utils
LOG = logging.getLogger(__name__)
auth_opts = [
cfg.StrOpt('auth_encryption_key',
@ -32,36 +38,52 @@ auth_opts = [
cfg.CONF.register_opts(auth_opts)
def encrypt(auth_info, encryption_key=None):
if auth_info is None:
def encrypt(value, encryption_key=None):
if value is None:
return None, None
encryption_key = get_valid_encryption_key(encryption_key)
sym = utils.SymmetricCrypto()
res = sym.encrypt(encryption_key,
auth_info, b64encode=True)
return 'oslo_decrypt_v1', res
sym = fernet.Fernet(encryption_key.encode('base64'))
res = sym.encrypt(encodeutils.safe_encode(value))
return 'cryptography_decrypt_v1', res
def oslo_decrypt_v1(auth_info, encryption_key=None):
if auth_info is None:
def decrypt(method, data, encryption_key=None):
if method is None or data is None:
return None
decryptor = getattr(sys.modules[__name__], method)
value = decryptor(data, encryption_key)
if value is not None:
try:
return encodeutils.safe_decode(value, 'utf-8')
except UnicodeDecodeError as ex:
# if the incorrect encryption_key was used then we can get
# total gibberish here and safe_decode() will freak out.
LOG.warn(_LW("Couldn't decrypt parameters %s"), ex)
def oslo_decrypt_v1(value, encryption_key=None):
encryption_key = get_valid_encryption_key(encryption_key)
sym = utils.SymmetricCrypto()
return sym.decrypt(encryption_key,
auth_info, b64decode=True)
value, b64decode=True)
def cryptography_decrypt_v1(value, encryption_key=None):
encryption_key = get_valid_encryption_key(encryption_key)
sym = fernet.Fernet(encryption_key.encode('base64'))
return sym.decrypt(encodeutils.safe_encode(value))
def get_valid_encryption_key(encryption_key):
if encryption_key is None:
encryption_key = cfg.CONF.auth_encryption_key[:32]
else:
encryption_key = encryption_key[0:32]
encryption_key = encryption_key[:32]
return encryption_key
def heat_decrypt(auth_info, encryption_key=None):
def heat_decrypt(value, encryption_key=None):
"""Decrypt function for data that has been encrypted using an older
version of Heat.
Note: the encrypt function returns the function that is needed to
@ -70,11 +92,8 @@ def heat_decrypt(auth_info, encryption_key=None):
function must still exist. So whilst it may seem that this function
is not referenced, it will be referenced from the database.
"""
if auth_info is None:
return None
encryption_key = get_valid_encryption_key(encryption_key)
auth = base64.b64decode(auth_info)
auth = base64.b64decode(value)
iv = auth[:AES.block_size]
cipher = AES.new(encryption_key, AES.MODE_CFB, iv)
res = cipher.decrypt(auth[AES.block_size:])

View File

@ -18,8 +18,6 @@ import sys
from oslo_config import cfg
from oslo_db.sqlalchemy import session as db_session
from oslo_db.sqlalchemy import utils
from oslo_log import log as logging
from oslo_utils import encodeutils
from oslo_utils import timeutils
import osprofiler.sqlalchemy
import six
@ -31,7 +29,6 @@ from sqlalchemy.orm import session as orm_session
from heat.common import crypt
from heat.common import exception
from heat.common.i18n import _
from heat.common.i18n import _LW
from heat.db.sqlalchemy import filters as db_filters
from heat.db.sqlalchemy import migration
from heat.db.sqlalchemy import models
@ -42,8 +39,6 @@ CONF.import_opt('hidden_stack_tags', 'heat.common.config')
CONF.import_opt('max_events_per_stack', 'heat.common.config')
CONF.import_group('profiler', 'heat.common.config')
LOG = logging.getLogger(__name__)
_facade = None
@ -197,7 +192,7 @@ def resource_data_get_all(resource, data=None):
for res in data:
if res.redact:
ret[res.key] = _decrypt(res.value, res.decrypt_method)
ret[res.key] = crypt.decrypt(res.decrypt_method, res.value)
else:
ret[res.key] = res.value
return ret
@ -211,7 +206,7 @@ def resource_data_get(resource, key):
resource.id,
key)
if result.redact:
return _decrypt(result.value, result.decrypt_method)
return crypt.decrypt(result.decrypt_method, result.value)
return result.value
@ -245,22 +240,6 @@ def stack_tags_get(context, stack_id):
return result or None
def _encrypt(value):
if value is not None:
return crypt.encrypt(value.encode('utf-8'))
else:
return None, None
def _decrypt(enc_value, method):
if method is None:
return None
decryptor = getattr(crypt, method)
value = decryptor(enc_value)
if value is not None:
return six.text_type(value, 'utf-8')
def resource_data_get_by_key(context, resource_id, key):
"""Looks up resource_data by resource_id and key. Does not unencrypt
resource_data.
@ -277,7 +256,7 @@ def resource_data_get_by_key(context, resource_id, key):
def resource_data_set(resource, key, value, redact=False):
"""Save resource's key/value pair to database."""
if redact:
method, value = _encrypt(value)
method, value = crypt.encrypt(value)
else:
method = ''
try:
@ -623,7 +602,7 @@ def user_creds_create(context):
values = context.to_dict()
user_creds_ref = models.UserCreds()
if values.get('trust_id'):
method, trust_id = _encrypt(values.get('trust_id'))
method, trust_id = crypt.encrypt(values.get('trust_id'))
user_creds_ref.trust_id = trust_id
user_creds_ref.decrypt_method = method
user_creds_ref.trustor_user_id = values.get('trustor_user_id')
@ -635,7 +614,7 @@ def user_creds_create(context):
user_creds_ref.region_name = values.get('region_name')
else:
user_creds_ref.update(values)
method, password = _encrypt(values['password'])
method, password = crypt.encrypt(values['password'])
if len(six.text_type(password)) > 255:
raise exception.Error(_("Length of OS_PASSWORD after encryption"
" exceeds Heat limit (255 chars)"))
@ -653,8 +632,10 @@ def user_creds_get(user_creds_id):
# or it can be committed back to the DB in decrypted form
result = dict(db_result)
del result['decrypt_method']
result['password'] = _decrypt(result['password'], db_result.decrypt_method)
result['trust_id'] = _decrypt(result['trust_id'], db_result.decrypt_method)
result['password'] = crypt.decrypt(
db_result.decrypt_method, result['password'])
result['trust_id'] = crypt.decrypt(
db_result.decrypt_method, result['trust_id'])
return result
@ -1159,8 +1140,7 @@ def db_encrypt_parameters_and_properties(ctxt, encryption_key):
except KeyError:
param_val = param.default
encoded_val = encodeutils.safe_encode(param_val)
encrypted_val = crypt.encrypt(encoded_val, encryption_key)
encrypted_val = crypt.encrypt(param_val, encryption_key)
raw_template.environment['parameters'][param_name] = \
encrypted_val
encrypted_params.append(param_name)
@ -1183,18 +1163,10 @@ def db_decrypt_parameters_and_properties(ctxt, encryption_key):
encrypted_params = raw_template.environment[
'encrypted_param_names']
for param_name in encrypted_params:
decrypt_function_name = parameters[param_name][0]
decrypt_function = getattr(crypt, decrypt_function_name)
decrypted_val = decrypt_function(parameters[param_name][1],
encryption_key)
try:
parameters[param_name] = encodeutils.safe_decode(
decrypted_val)
except UnicodeDecodeError as ex:
# if the incorrect encryption_key was used then we can get
# total gibberish here and safe_decode() will freak out.
LOG.warn(_LW("Couldn't decrypt parameters %s"), ex)
parameters[param_name] = ""
method, value = parameters[param_name]
decrypted_val = crypt.decrypt(method, value, encryption_key)
parameters[param_name] = decrypted_val
environment = raw_template.environment.copy()
environment['encrypted_param_names'] = []
raw_template_update(ctxt, raw_template.id,

View File

@ -20,7 +20,6 @@ RawTemplate object
import copy
from oslo_config import cfg
from oslo_utils import encodeutils
from oslo_versionedobjects import base
from oslo_versionedobjects import fields
@ -56,10 +55,9 @@ class RawTemplate(
env_fmt.ENCRYPTED_PARAM_NAMES]
for param_name in encrypted_param_names:
decrypt_function_name = parameters[param_name][0]
decrypt_function = getattr(crypt, decrypt_function_name)
decrypted_val = decrypt_function(parameters[param_name][1])
parameters[param_name] = encodeutils.safe_decode(decrypted_val)
method, value = parameters[param_name]
decrypted_val = crypt.decrypt(method, value)
parameters[param_name] = decrypted_val
tpl.environment[env_fmt.PARAMETERS] = parameters
tpl._context = context
@ -78,8 +76,7 @@ class RawTemplate(
if not tmpl.param_schemata()[param_name].hidden:
continue
clear_text_val = tmpl.env.params.get(param_name)
encoded_val = encodeutils.safe_encode(clear_text_val)
tmpl.env.params[param_name] = crypt.encrypt(encoded_val)
tmpl.env.params[param_name] = crypt.encrypt(clear_text_val)
tmpl.env.encrypted_param_names.append(param_name)
@classmethod

View File

@ -20,7 +20,6 @@ Resource object
from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
from oslo_versionedobjects import base
from oslo_versionedobjects import fields
import six
@ -85,9 +84,8 @@ class Resource(
if resource.properties_data_encrypted and resource.properties_data:
properties_data = {}
for prop_name, prop_value in resource.properties_data.items():
decrypt_function_name = prop_value[0]
decrypt_function = getattr(crypt, decrypt_function_name, None)
decrypted_value = decrypt_function(prop_value[1])
method, value = prop_value
decrypted_value = crypt.decrypt(method, value)
prop_string = jsonutils.loads(decrypted_value)
properties_data[prop_name] = prop_string
resource.properties_data = properties_data
@ -185,8 +183,7 @@ class Resource(
result = {}
for prop_name, prop_value in data.items():
prop_string = jsonutils.dumps(prop_value)
encoded_value = encodeutils.safe_encode(prop_string)
encrypted_value = crypt.encrypt(encoded_value)
encrypted_value = crypt.encrypt(prop_string)
result[prop_name] = encrypted_value
return (True, result)
return (False, data)

View File

@ -22,6 +22,7 @@ from oslo_utils import timeutils
import six
from heat.common import context
from heat.common import crypt
from heat.common import exception
from heat.common import template_format
from heat.db.sqlalchemy import api as db_api
@ -1500,8 +1501,8 @@ class DBAPIUserCredsTest(common.HeatTestCase):
trustor_user_id='trustor_id')
self.assertIsNotNone(user_creds.id)
self.assertEqual('test_trust_id',
db_api._decrypt(user_creds.trust_id,
user_creds.decrypt_method))
crypt.decrypt(user_creds.decrypt_method,
user_creds.trust_id))
self.assertEqual('trustor_id', user_creds.trustor_user_id)
self.assertIsNone(user_creds.username)
self.assertIsNone(user_creds.password)
@ -1512,14 +1513,14 @@ class DBAPIUserCredsTest(common.HeatTestCase):
user_creds = create_user_creds(self.ctx)
self.assertIsNotNone(user_creds.id)
self.assertEqual(self.ctx.password,
db_api._decrypt(user_creds.password,
user_creds.decrypt_method))
crypt.decrypt(user_creds.decrypt_method,
user_creds.password))
def test_user_creds_get(self):
user_creds = create_user_creds(self.ctx)
ret_user_creds = db_api.user_creds_get(user_creds.id)
self.assertEqual(db_api._decrypt(user_creds.password,
user_creds.decrypt_method),
self.assertEqual(crypt.decrypt(user_creds.decrypt_method,
user_creds.password),
ret_user_creds['password'])
def test_user_creds_get_noexist(self):
@ -2731,8 +2732,9 @@ class DBAPICryptParamsPropsTest(common.HeatTestCase):
self.ctx, cfg.CONF.auth_encryption_key)
env = session.query(models.RawTemplate).all()[0].environment
self.assertEqual('oslo_decrypt_v1',
self.assertEqual('cryptography_decrypt_v1',
env['parameters']['param2'][0])
encrypt_value = env['parameters']['param2'][1]
db_api.db_decrypt_parameters_and_properties(
self.ctx, cfg.CONF.auth_encryption_key)
@ -2742,9 +2744,13 @@ class DBAPICryptParamsPropsTest(common.HeatTestCase):
# Use a different encryption key to decrypt
db_api.db_encrypt_parameters_and_properties(
self.ctx, cfg.CONF.auth_encryption_key)
self.ctx, '774c15be099ea74123a9b9592ff12680')
env = session.query(models.RawTemplate).all()[0].environment
self.assertNotEqual(encrypt_value,
env['parameters']['param2'][1])
db_api.db_decrypt_parameters_and_properties(
self.ctx, '774c15be099ea74123a9b9592ff12680')
env = session.query(models.RawTemplate).all()[0].environment
self.assertNotEqual('bar', env['parameters']['param2'])
self.assertEqual('bar', env['parameters']['param2'])

View File

@ -1987,7 +1987,7 @@ class StackTest(common.HeatTestCase):
db_tpl = db_api.raw_template_get(self.ctx, self.stack.t.id)
db_params = db_tpl.environment['parameters']
self.assertEqual('foo', db_params['param1'])
self.assertEqual('oslo_decrypt_v1', db_params['param2'][0])
self.assertEqual('cryptography_decrypt_v1', db_params['param2'][0])
self.assertIsNotNone(db_params['param2'][1])
# Verify that loaded stack has decrypted paramters

View File

@ -4,6 +4,7 @@
pbr<2.0,>=0.11
Babel>=1.3
cryptography>=0.8.2 # Apache-2.0
dogpile.cache>=0.5.3
eventlet>=0.17.4
greenlet>=0.3.2