172 lines
6.2 KiB
Python
172 lines
6.2 KiB
Python
# Copyright (c) 2015 OpenStack Foundation
|
|
#
|
|
# 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 binascii
|
|
from hashlib import md5
|
|
import os
|
|
|
|
from cryptography.hazmat.backends import default_backend
|
|
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
|
|
|
|
from swift.common.exceptions import EncryptionException
|
|
from swift.common.utils import get_logger
|
|
|
|
# AES will accept several key sizes - we are using 256 bits i.e. 32 bytes
|
|
KEY_LENGTH = 32
|
|
|
|
|
|
class Crypto(object):
|
|
"""
|
|
Used by middleware: Calls crypto alg
|
|
"""
|
|
def __init__(self, conf=None):
|
|
conf = {} if conf is None else conf
|
|
self.logger = get_logger(conf, log_route="crypto")
|
|
|
|
def create_encryption_ctxt(self, key, iv):
|
|
"""
|
|
Creates a crypto context for encrypting
|
|
|
|
:param key: 256-bit key
|
|
:param iv: 128-bit iv or nonce used for encryption
|
|
:raises: ValueError on invalid key or iv
|
|
:returns: an instance of :class:`CryptoContext`
|
|
"""
|
|
self.check_key(key)
|
|
engine = Cipher(algorithms.AES(key), modes.CTR(iv),
|
|
backend=default_backend())
|
|
enc = engine.encryptor()
|
|
return CryptoContext(enc, iv, 0)
|
|
|
|
def create_decryption_ctxt(self, key, iv, offset):
|
|
"""
|
|
Creates a crypto context for decrypting
|
|
|
|
:param key: 256-bit key
|
|
:param iv: 128-bit iv or nonce used for decryption
|
|
:param offset: offset into the message; used for range reads
|
|
:returns: an instance of :class:`CryptoContext`
|
|
"""
|
|
self.check_key(key)
|
|
if offset < 0:
|
|
raise ValueError('Offset must not be negative')
|
|
if offset > 0:
|
|
# Adjust IV so that it is correct for decryption at offset.
|
|
# ( 1<< (16 *8)) is to make 'ivl' big enough so that the following
|
|
# bytearray.fromhex() can be successful in all conditions.
|
|
ivl = long(binascii.hexlify(iv), 16)
|
|
ivl += int(offset / 16) + (1 << (16 * 8))
|
|
ivstr = format(ivl, 'x')
|
|
iv = str(bytearray.fromhex(ivstr[(len(ivstr) - 2 * 16):]))
|
|
|
|
engine = Cipher(algorithms.AES(key), modes.CTR(iv),
|
|
backend=default_backend())
|
|
dec = engine.decryptor()
|
|
# Adjust decryption boundary to AES block size of 16 bytes
|
|
dec.update('*' * (offset % 16))
|
|
return CryptoContext(dec, iv, offset)
|
|
|
|
def get_required_iv_length(self):
|
|
return algorithms.AES.block_size / 8
|
|
|
|
def _get_derived_iv(self, base):
|
|
target_length = self.get_required_iv_length()
|
|
if len(base) < target_length:
|
|
return base.zfill(target_length)
|
|
elif len(base) > target_length:
|
|
hash = md5()
|
|
hash.update(base)
|
|
return hash.hexdigest()[-target_length:]
|
|
else:
|
|
return base
|
|
|
|
def _get_random_iv(self):
|
|
# this method is separated out here so that tests can mock it
|
|
return os.urandom(self.get_required_iv_length())
|
|
|
|
def create_iv(self, iv_base=None):
|
|
if iv_base:
|
|
return self._get_derived_iv(iv_base)
|
|
return self._get_random_iv()
|
|
|
|
def get_cipher(self):
|
|
return 'AES_CTR_256'
|
|
|
|
def create_crypto_meta(self, iv_base=None):
|
|
# create a set of parameters
|
|
return {'iv': self.create_iv(iv_base), 'cipher': self.get_cipher()}
|
|
|
|
def check_crypto_meta(self, meta):
|
|
"""
|
|
Check that crypto meta dict has valid items.
|
|
|
|
:param meta: a dict
|
|
:raises EncryptionException: if an error is found in the crypto meta
|
|
"""
|
|
try:
|
|
if meta['cipher'] != self.get_cipher():
|
|
raise EncryptionException('Bad crypto meta: Cipher must be %s'
|
|
% self.get_cipher())
|
|
if len(meta['iv']) != self.get_required_iv_length():
|
|
raise EncryptionException(
|
|
'Bad crypto meta: IV must be length %s bytes'
|
|
% self.get_required_iv_length())
|
|
except KeyError as err:
|
|
raise EncryptionException(
|
|
'Bad crypto meta: Missing %s' % err)
|
|
|
|
def create_random_key(self):
|
|
# helper method to create random key of correct length
|
|
return os.urandom(KEY_LENGTH)
|
|
|
|
def wrap_key(self, wrapping_key, key_to_wrap):
|
|
# we don't use an RFC 3394 key wrap algorithm such as cryptography's
|
|
# aes_wrap_key because it's slower and we have iv material readily
|
|
# available so don't need a deterministic algorithm
|
|
iv = self._get_random_iv()
|
|
encryptor = Cipher(algorithms.AES(wrapping_key), modes.CTR(iv),
|
|
backend=default_backend()).encryptor()
|
|
return {'key': encryptor.update(key_to_wrap), 'iv': iv}
|
|
|
|
def unwrap_key(self, wrapping_key, context):
|
|
# unwrap a key from dict of form returned by wrap_key
|
|
# check the key length early - unwrapping won't change the length
|
|
self.check_key(context['key'])
|
|
decryptor = Cipher(algorithms.AES(wrapping_key),
|
|
modes.CTR(context['iv']),
|
|
backend=default_backend()).decryptor()
|
|
return decryptor.update(context['key'])
|
|
|
|
def check_key(self, key):
|
|
if len(key) != KEY_LENGTH:
|
|
raise ValueError("Key must be length %s bytes" % KEY_LENGTH)
|
|
|
|
|
|
class CryptoContext(object):
|
|
"""
|
|
Crypto context used in encryption middleware. Created by calling
|
|
:func:`create_encryption_ctxt` or :func:`create_decryption_ctxt`.
|
|
"""
|
|
def __init__(self, engine, iv, offset):
|
|
self.engine = engine
|
|
self.iv = iv
|
|
self.offset = offset
|
|
|
|
def update(self, chunk):
|
|
return self.engine.update(chunk)
|
|
|
|
def get_iv(self):
|
|
return self.iv
|