heat/heat/common/crypt.py

164 lines
5.5 KiB
Python

#
# 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
import sys
from Crypto.Cipher import AES
from cryptography import fernet
from oslo_config import cfg
from oslo_serialization import jsonutils
from oslo_utils import encodeutils
from oslo_utils import importutils
from heat.common.i18n import _
auth_opts = [
cfg.StrOpt('auth_encryption_key',
secret=True,
default='notgood but just long enough i t',
help=_('Key used to encrypt authentication info in the '
'database. Length of this key must be 32 characters.'))
]
cfg.CONF.register_opts(auth_opts)
class SymmetricCrypto(object):
"""Symmetric Key Crypto object.
This class creates a Symmetric Key Crypto object that can be used
to decrypt arbitrary data.
Note: This is moved here from oslo-incubator for backward
compatibility. Once we've db migration script available to
re-rencrypt using new encryption method as part of upgrade,
this can be removed.
:param enctype: Encryption Cipher name (default: AES)
"""
def __init__(self, enctype='AES'):
self.cipher = importutils.import_module('Crypto.Cipher.' + enctype)
def decrypt(self, key, msg, b64decode=True):
"""Decrypts the provided ciphertext.
The ciphertext can be optionally base64 encoded.
Uses AES-128-CBC with an IV by default.
:param key: The Encryption key.
:param msg: the ciphetext, the first block is the IV
:returns plain: the plaintext message, after padding is removed.
"""
if b64decode:
msg = base64.b64decode(msg)
iv = msg[:self.cipher.block_size]
cipher = self.cipher.new(key, self.cipher.MODE_CBC, iv)
padded = cipher.decrypt(msg[self.cipher.block_size:])
l = ord(padded[-1:]) + 1
plain = padded[:-l]
return plain
def encrypt(value, encryption_key=None):
if value is None:
return None, None
encryption_key = get_valid_encryption_key(encryption_key, fix_length=True)
encoded_key = base64.b64encode(encryption_key.encode('utf-8'))
sym = fernet.Fernet(encoded_key)
res = sym.encrypt(encodeutils.safe_encode(value))
return 'cryptography_decrypt_v1', encodeutils.safe_decode(res)
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:
return encodeutils.safe_decode(value, 'utf-8')
def encrypted_dict(data, encryption_key=None):
'Return an encrypted dict. Values converted to json before encrypted'
return_data = {}
if not data:
return return_data
for prop_name, prop_value in data.items():
prop_string = jsonutils.dumps(prop_value)
encrypted_value = encrypt(prop_string, encryption_key)
return_data[prop_name] = encrypted_value
return return_data
def decrypted_dict(data, encryption_key=None):
'Return a decrypted dict. Assume input values are encrypted json fields.'
return_data = {}
if not data:
return return_data
for prop_name, prop_value in data.items():
method, value = prop_value
decrypted_value = decrypt(method, value, encryption_key)
prop_string = jsonutils.loads(decrypted_value)
return_data[prop_name] = prop_string
return return_data
def oslo_decrypt_v1(value, encryption_key=None):
encryption_key = get_valid_encryption_key(encryption_key)
sym = SymmetricCrypto()
return sym.decrypt(encryption_key, value, b64decode=True)
def cryptography_decrypt_v1(value, encryption_key=None):
encryption_key = get_valid_encryption_key(encryption_key, fix_length=True)
encoded_key = base64.b64encode(encryption_key.encode('utf-8'))
sym = fernet.Fernet(encoded_key)
return sym.decrypt(encodeutils.safe_encode(value))
def get_valid_encryption_key(encryption_key, fix_length=False):
if encryption_key is None:
encryption_key = cfg.CONF.auth_encryption_key
if fix_length and len(encryption_key) < 32:
# Backward compatible size
encryption_key = encryption_key * 2
return encryption_key[:32]
def heat_decrypt(value, encryption_key=None):
"""Decrypt data that has been encrypted using an older version of Heat.
Note: the encrypt function returns the function that is needed to
decrypt the data. The database then stores this. When the data is
then retrieved (potentially by a later version of Heat) the decrypt
function must still exist. So whilst it may seem that this function
is not referenced, it will be referenced from the database.
"""
encryption_key = get_valid_encryption_key(encryption_key)
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:])
return res
def list_opts():
yield None, auth_opts