crypto - use random iv when wrapping body key

Change-Id: Ia32a7b1cbafd5f593d0609310e4a38de6c52f220
This commit is contained in:
Alistair Coles 2016-05-25 21:21:33 +01:00
parent 4728e3e8d3
commit 2cec70530b
9 changed files with 90 additions and 62 deletions

View File

@ -88,11 +88,16 @@ def dump_crypto_meta(crypto_meta):
:param crypto_meta: a dict containing crypto meta items :param crypto_meta: a dict containing crypto meta items
:returns: a string serialization of a crypto meta dict :returns: a string serialization of a crypto meta dict
""" """
def b64_encode_meta(crypto_meta):
return {
name: (base64.b64encode(value).decode() if name in ('iv', 'key')
else b64_encode_meta(value) if isinstance(value, dict)
else value)
for name, value in crypto_meta.items()}
# use sort_keys=True to make serialized form predictable for testing # use sort_keys=True to make serialized form predictable for testing
return urllib.quote_plus(json.dumps({ return urllib.quote_plus(
name: (base64.b64encode(value).decode() if name in ('iv', 'key') json.dumps(b64_encode_meta(crypto_meta), sort_keys=True))
else value)
for name, value in crypto_meta.items()}, sort_keys=True))
def load_crypto_meta(value): def load_crypto_meta(value):
@ -110,12 +115,16 @@ def load_crypto_meta(value):
:raises EncryptionException: if an error occurs while parsing the :raises EncryptionException: if an error occurs while parsing the
crypto meta crypto meta
""" """
def b64_decode_meta(crypto_meta):
return {
str(name): (base64.b64decode(val) if name in ('iv', 'key')
else b64_decode_meta(val) if isinstance(val, dict)
else str(val))
for name, val in crypto_meta.items()}
try: try:
value = urllib.unquote_plus(value) value = urllib.unquote_plus(value)
crypto_meta = {str(name): (base64.b64decode(value) return b64_decode_meta(json.loads(value))
if name in ('iv', 'key') else str(value))
for name, value in json.loads(value).items()}
return crypto_meta
except (KeyError, ValueError, TypeError) as err: except (KeyError, ValueError, TypeError) as err:
msg = 'Bad crypto meta %s: %s' % (value, err) msg = 'Bad crypto meta %s: %s' % (value, err)
raise EncryptionException(msg) raise EncryptionException(msg)

View File

@ -131,21 +131,23 @@ class Crypto(object):
# helper method to create random key of correct length # helper method to create random key of correct length
return os.urandom(KEY_LENGTH) return os.urandom(KEY_LENGTH)
def wrap_key(self, wrapping_key, key_to_wrap, iv): def wrap_key(self, wrapping_key, key_to_wrap):
# we don't use an RFC 3394 key wrap algorithm such as cryptography's # 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 # aes_wrap_key because it's slower and we have iv material readily
# available so don't need a deterministic algorithm # available so don't need a deterministic algorithm
iv = self._get_random_iv()
encryptor = Cipher(algorithms.AES(wrapping_key), modes.CTR(iv), encryptor = Cipher(algorithms.AES(wrapping_key), modes.CTR(iv),
backend=default_backend()).encryptor() backend=default_backend()).encryptor()
return encryptor.update(key_to_wrap) return {'key': encryptor.update(key_to_wrap), 'iv': iv}
def unwrap_key(self, wrapping_key, wrapped_key, 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 # check the key length early - unwrapping won't change the length
self.check_key(wrapped_key) self.check_key(context['key'])
decryptor = Cipher(algorithms.AES(wrapping_key), modes.CTR(iv), decryptor = Cipher(algorithms.AES(wrapping_key),
modes.CTR(context['iv']),
backend=default_backend()).decryptor() backend=default_backend()).decryptor()
key = decryptor.update(wrapped_key) return decryptor.update(context['key'])
return key
def check_key(self, key): def check_key(self, key):
if len(key) != KEY_LENGTH: if len(key) != KEY_LENGTH:

View File

@ -70,8 +70,7 @@ class BaseDecrypterContext(CryptoWSGIContext):
""" """
try: try:
return self.crypto.unwrap_key(wrapping_key, return self.crypto.unwrap_key(wrapping_key,
crypto_meta['key'], crypto_meta['body_key'])
crypto_meta['iv'])
except KeyError as err: except KeyError as err:
err = 'Missing %s' % err err = 'Missing %s' % err
except ValueError as err: except ValueError as err:

View File

@ -68,15 +68,12 @@ class EncInputWrapper(object):
# do this once when body is first read # do this once when body is first read
if self.body_crypto_ctxt is None: if self.body_crypto_ctxt is None:
self.body_crypto_meta = self.crypto.create_crypto_meta() self.body_crypto_meta = self.crypto.create_crypto_meta()
self.body_key = self.crypto.create_random_key() body_key = self.crypto.create_random_key()
# wrap the body key with object key re-using body iv # wrap the body key with object key
self.body_crypto_meta['key'] = self.crypto.wrap_key( self.body_crypto_meta['body_key'] = self.crypto.wrap_key(
self.keys['object'], self.keys['object'], body_key)
self.body_key,
self.body_crypto_meta['iv']
)
self.body_crypto_ctxt = self.crypto.create_encryption_ctxt( self.body_crypto_ctxt = self.crypto.create_encryption_ctxt(
self.body_key, self.body_crypto_meta.get('iv')) body_key, self.body_crypto_meta.get('iv'))
self.plaintext_md5 = md5() self.plaintext_md5 = md5()
self.ciphertext_md5 = md5() self.ciphertext_md5 = md5()

View File

@ -12,8 +12,7 @@
# implied. # implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
import mock
import unittest import unittest
import os import os
@ -194,24 +193,26 @@ class TestCrypto(unittest.TestCase):
wrapping_key = os.urandom(32) wrapping_key = os.urandom(32)
key_to_wrap = os.urandom(32) key_to_wrap = os.urandom(32)
iv = os.urandom(16) iv = os.urandom(16)
wrapped = self.crypto.wrap_key(wrapping_key, key_to_wrap, iv) with mock.patch('swift.common.middleware.crypto.Crypto._get_random_iv',
return_value=iv):
wrapped = self.crypto.wrap_key(wrapping_key, key_to_wrap)
cipher = Cipher(algorithms.AES(wrapping_key), modes.CTR(iv), cipher = Cipher(algorithms.AES(wrapping_key), modes.CTR(iv),
backend=default_backend()) backend=default_backend())
expected = cipher.encryptor().update(key_to_wrap) expected = {'key': cipher.encryptor().update(key_to_wrap),
'iv': iv}
self.assertEqual(expected, wrapped) self.assertEqual(expected, wrapped)
unwrapped = self.crypto.unwrap_key(wrapping_key, wrapped, iv) unwrapped = self.crypto.unwrap_key(wrapping_key, wrapped)
self.assertEqual(key_to_wrap, unwrapped) self.assertEqual(key_to_wrap, unwrapped)
def test_unwrap_bad_key(self): def test_unwrap_bad_key(self):
# verify that ValueError is raised if unwrapped key is invalid # verify that ValueError is raised if unwrapped key is invalid
wrapping_key = os.urandom(32) wrapping_key = os.urandom(32)
iv = os.urandom(16)
for length in (0, 16, 24, 31, 33): for length in (0, 16, 24, 31, 33):
key_to_wrap = os.urandom(length) key_to_wrap = os.urandom(length)
wrapped = self.crypto.wrap_key(wrapping_key, key_to_wrap, iv) wrapped = self.crypto.wrap_key(wrapping_key, key_to_wrap)
with self.assertRaises(ValueError) as cm: with self.assertRaises(ValueError) as cm:
self.crypto.unwrap_key(wrapping_key, wrapped, iv) self.crypto.unwrap_key(wrapping_key, wrapped)
self.assertEqual( self.assertEqual(
cm.exception.message, 'Key must be length 32 bytes') cm.exception.message, 'Key must be length 32 bytes')

View File

@ -149,11 +149,14 @@ class TestModuleMethods(unittest.TestCase):
'iv%22%3A+%22MDEyMzQ1Njc4OWFiY2RlZg%3D%3D%22%7D' 'iv%22%3A+%22MDEyMzQ1Njc4OWFiY2RlZg%3D%3D%22%7D'
meta_with_key = {'iv': '0123456789abcdef', 'cipher': 'AES_CTR_256', meta_with_key = {'iv': '0123456789abcdef', 'cipher': 'AES_CTR_256',
'key': '0123456789abcdef0123456789abcdef'} 'body_key': {'key': 'fedcba9876543210fedcba9876543210',
serialized_meta_with_key = '%7B%22cipher%22%3A+%22AES_CTR_256%22%2C+%22' \ 'iv': 'fedcba9876543210'}}
'iv%22%3A+%22MDEyMzQ1Njc4OWFiY2RlZg%3D%3D%22' \ serialized_meta_with_key = '%7B%22body_key%22%3A+%7B%22iv%22%3A+%22ZmVkY' \
'%2C+%22key%22%3A+%22MDEyMzQ1Njc4OWFiY2RlZjA' \ '2JhOTg3NjU0MzIxMA%3D%3D%22%2C+%22key%22%3A+%' \
'xMjM0NTY3ODlhYmNkZWY%3D%22%7D' '22ZmVkY2JhOTg3NjU0MzIxMGZlZGNiYTk4NzY1NDMyMT' \
'A%3D%22%7D%2C+%22cipher%22%3A+%22AES_CTR_256' \
'%22%2C+%22iv%22%3A+%22MDEyMzQ1Njc4OWFiY2RlZg' \
'%3D%3D%22%7D'
def test_dump_crypto_meta(self): def test_dump_crypto_meta(self):
actual = crypto_utils.dump_crypto_meta(self.meta) actual = crypto_utils.dump_crypto_meta(self.meta)

View File

@ -61,8 +61,9 @@ class TestDecrypterObjectRequests(unittest.TestCase):
object_key = fetch_crypto_keys()['object'] object_key = fetch_crypto_keys()['object']
body_key = os.urandom(32) body_key = os.urandom(32)
enc_body = encrypt(body, body_key, FAKE_IV) enc_body = encrypt(body, body_key, FAKE_IV)
body_crypto_meta = fake_get_crypto_meta( body_key_meta = {'key': encrypt(body_key, object_key, FAKE_IV),
key=encrypt(body_key, object_key, FAKE_IV)) 'iv': FAKE_IV}
body_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
hdrs = { hdrs = {
'Etag': 'hashOfCiphertext', 'Etag': 'hashOfCiphertext',
'content-type': 'text/plain', 'content-type': 'text/plain',
@ -313,7 +314,8 @@ class TestDecrypterObjectRequests(unittest.TestCase):
self.decrypter.logger.get_lines_for_level('error')[0]) self.decrypter.logger.get_lines_for_level('error')[0])
def test_GET_with_bad_body_key_for_object_body(self): def test_GET_with_bad_body_key_for_object_body(self):
bad_crypto_meta = fake_get_crypto_meta(key='wrapped too short key') body_key_meta = {'key': 'wrapped too short key', 'iv': FAKE_IV}
bad_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
self._test_GET_with_bad_crypto_meta_for_object_body(bad_crypto_meta) self._test_GET_with_bad_crypto_meta_for_object_body(bad_crypto_meta)
self.assertIn('Key must be length 32', self.assertIn('Key must be length 32',
self.decrypter.logger.get_lines_for_level('error')[0]) self.decrypter.logger.get_lines_for_level('error')[0])
@ -321,7 +323,7 @@ class TestDecrypterObjectRequests(unittest.TestCase):
def test_GET_with_missing_body_key_for_object_body(self): def test_GET_with_missing_body_key_for_object_body(self):
bad_crypto_meta = fake_get_crypto_meta() # no key by default bad_crypto_meta = fake_get_crypto_meta() # no key by default
self._test_GET_with_bad_crypto_meta_for_object_body(bad_crypto_meta) self._test_GET_with_bad_crypto_meta_for_object_body(bad_crypto_meta)
self.assertIn("Missing 'key'", self.assertIn("Missing 'body_key'",
self.decrypter.logger.get_lines_for_level('error')[0]) self.decrypter.logger.get_lines_for_level('error')[0])
def test_HEAD_success(self): def test_HEAD_success(self):
@ -368,8 +370,9 @@ class TestDecrypterObjectRequests(unittest.TestCase):
object_key = fetch_crypto_keys()['object'] object_key = fetch_crypto_keys()['object']
body_key = os.urandom(32) body_key = os.urandom(32)
enc_body = encrypt(body, body_key, FAKE_IV) enc_body = encrypt(body, body_key, FAKE_IV)
body_crypto_meta = fake_get_crypto_meta( body_key_meta = {'key': encrypt(body_key, object_key, FAKE_IV),
key=encrypt(body_key, object_key, FAKE_IV)) 'iv': FAKE_IV}
body_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
hdrs = { hdrs = {
'Etag': 'hashOfCiphertext', 'Etag': 'hashOfCiphertext',
'etag': 'hashOfCiphertext', 'etag': 'hashOfCiphertext',
@ -405,8 +408,9 @@ class TestDecrypterObjectRequests(unittest.TestCase):
object_key = fetch_crypto_keys()['object'] object_key = fetch_crypto_keys()['object']
body_key = os.urandom(32) body_key = os.urandom(32)
enc_body = encrypt(body, body_key, FAKE_IV) enc_body = encrypt(body, body_key, FAKE_IV)
body_crypto_meta = fake_get_crypto_meta( body_key_meta = {'key': encrypt(body_key, object_key, FAKE_IV),
key=encrypt(body_key, object_key, FAKE_IV)) 'iv': FAKE_IV}
body_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
hdrs = { hdrs = {
'Etag': 'hashOfCiphertext', 'Etag': 'hashOfCiphertext',
'etag': 'hashOfCiphertext', 'etag': 'hashOfCiphertext',
@ -471,8 +475,9 @@ class TestDecrypterObjectRequests(unittest.TestCase):
cont_key = fetch_crypto_keys()['container'] cont_key = fetch_crypto_keys()['container']
object_key = fetch_crypto_keys()['object'] object_key = fetch_crypto_keys()['object']
body_key = os.urandom(32) body_key = os.urandom(32)
body_crypto_meta = fake_get_crypto_meta( body_key_meta = {'key': encrypt(body_key, object_key, FAKE_IV),
key=encrypt(body_key, object_key, FAKE_IV)) 'iv': FAKE_IV}
body_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
ctxt = Crypto().create_encryption_ctxt(body_key, FAKE_IV) ctxt = Crypto().create_encryption_ctxt(body_key, FAKE_IV)
enc_body = [encrypt(chunk, ctxt=ctxt) for chunk in chunks] enc_body = [encrypt(chunk, ctxt=ctxt) for chunk in chunks]
hdrs = { hdrs = {
@ -503,8 +508,9 @@ class TestDecrypterObjectRequests(unittest.TestCase):
cont_key = fetch_crypto_keys()['container'] cont_key = fetch_crypto_keys()['container']
object_key = fetch_crypto_keys()['object'] object_key = fetch_crypto_keys()['object']
body_key = os.urandom(32) body_key = os.urandom(32)
body_crypto_meta = fake_get_crypto_meta( body_key_meta = {'key': encrypt(body_key, object_key, FAKE_IV),
key=encrypt(body_key, object_key, FAKE_IV)) 'iv': FAKE_IV}
body_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
ctxt = Crypto().create_encryption_ctxt(body_key, FAKE_IV) ctxt = Crypto().create_encryption_ctxt(body_key, FAKE_IV)
enc_body = [encrypt(chunk, ctxt=ctxt) for chunk in chunks] enc_body = [encrypt(chunk, ctxt=ctxt) for chunk in chunks]
enc_body = [enc_body[0][3:], enc_body[1], enc_body[2][:2]] enc_body = [enc_body[0][3:], enc_body[1], enc_body[2][:2]]
@ -539,8 +545,9 @@ class TestDecrypterObjectRequests(unittest.TestCase):
cont_key = fetch_crypto_keys()['container'] cont_key = fetch_crypto_keys()['container']
object_key = fetch_crypto_keys()['object'] object_key = fetch_crypto_keys()['object']
body_key = os.urandom(32) body_key = os.urandom(32)
body_crypto_meta = fake_get_crypto_meta( body_key_meta = {'key': encrypt(body_key, object_key, FAKE_IV),
key=encrypt(body_key, object_key, FAKE_IV)) 'iv': FAKE_IV}
body_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext = 'Cwm fjord veg balks nth pyx quiz'
plaintext_etag = md5hex(plaintext) plaintext_etag = md5hex(plaintext)
ciphertext = encrypt(plaintext, body_key, FAKE_IV) ciphertext = encrypt(plaintext, body_key, FAKE_IV)
@ -605,8 +612,9 @@ class TestDecrypterObjectRequests(unittest.TestCase):
cont_key = fetch_crypto_keys()['container'] cont_key = fetch_crypto_keys()['container']
object_key = fetch_crypto_keys()['object'] object_key = fetch_crypto_keys()['object']
body_key = os.urandom(32) body_key = os.urandom(32)
body_crypto_meta = fake_get_crypto_meta( body_key_meta = {'key': encrypt(body_key, object_key, FAKE_IV),
key=encrypt(body_key, object_key, FAKE_IV)) 'iv': FAKE_IV}
body_crypto_meta = fake_get_crypto_meta(body_key=body_key_meta)
plaintext = 'Cwm fjord veg balks nth pyx quiz' plaintext = 'Cwm fjord veg balks nth pyx quiz'
plaintext_etag = md5hex(plaintext) plaintext_etag = md5hex(plaintext)
ciphertext = encrypt(plaintext, body_key, FAKE_IV) ciphertext = encrypt(plaintext, body_key, FAKE_IV)

View File

@ -75,10 +75,15 @@ class TestEncrypter(unittest.TestCase):
# verify body crypto meta # verify body crypto meta
actual = req_hdrs['X-Object-Sysmeta-Crypto-Meta'] actual = req_hdrs['X-Object-Sysmeta-Crypto-Meta']
actual = json.loads(urllib.unquote_plus(actual)) actual = json.loads(urllib.unquote_plus(actual))
expected_wrapped_key = encrypt(body_key, object_key, FAKE_IV)
self.assertEqual(Crypto().get_cipher(), actual['cipher']) self.assertEqual(Crypto().get_cipher(), actual['cipher'])
self.assertEqual(FAKE_IV, base64.b64decode(actual['iv'])) self.assertEqual(FAKE_IV, base64.b64decode(actual['iv']))
self.assertEqual(expected_wrapped_key, base64.b64decode(actual['key']))
# verify wrapped body key
expected_wrapped_key = encrypt(body_key, object_key, FAKE_IV)
self.assertEqual(expected_wrapped_key,
base64.b64decode(actual['body_key']['key']))
self.assertEqual(FAKE_IV,
base64.b64decode(actual['body_key']['iv']))
# verify etag # verify etag
self.assertEqual(ciphertext_etag, req_hdrs['Etag']) self.assertEqual(ciphertext_etag, req_hdrs['Etag'])
@ -285,10 +290,15 @@ class TestEncrypter(unittest.TestCase):
# verify body crypto meta # verify body crypto meta
actual = req_hdrs['X-Object-Sysmeta-Crypto-Meta'] actual = req_hdrs['X-Object-Sysmeta-Crypto-Meta']
actual = json.loads(urllib.unquote_plus(actual)) actual = json.loads(urllib.unquote_plus(actual))
expected_wrapped_key = encrypt(body_key, object_key, FAKE_IV)
self.assertEqual(Crypto().get_cipher(), actual['cipher']) self.assertEqual(Crypto().get_cipher(), actual['cipher'])
self.assertEqual(FAKE_IV, base64.b64decode(actual['iv'])) self.assertEqual(FAKE_IV, base64.b64decode(actual['iv']))
self.assertEqual(expected_wrapped_key, base64.b64decode(actual['key']))
# verify wrapped body key
expected_wrapped_key = encrypt(body_key, object_key, FAKE_IV)
self.assertEqual(expected_wrapped_key,
base64.b64decode(actual['body_key']['key']))
self.assertEqual(FAKE_IV,
base64.b64decode(actual['body_key']['iv']))
def test_PUT_with_etag_override_in_headers(self): def test_PUT_with_etag_override_in_headers(self):
# verify handling of another middleware's # verify handling of another middleware's

View File

@ -300,11 +300,10 @@ class TestCryptoPipelineChanges(unittest.TestCase):
# verify on disk data - body # verify on disk data - body
body_iv = load_crypto_meta( body_iv = load_crypto_meta(
metadata['x-object-sysmeta-crypto-meta'])['iv'] metadata['x-object-sysmeta-crypto-meta'])['iv']
wrapped_body_key = load_crypto_meta( body_key_meta = load_crypto_meta(
metadata['x-object-sysmeta-crypto-meta'])['key'] metadata['x-object-sysmeta-crypto-meta'])['body_key']
obj_key = self.km.create_key('/a/%s/o' % self.container_name) obj_key = self.km.create_key('/a/%s/o' % self.container_name)
body_key = crypto.Crypto({}).unwrap_key( body_key = crypto.Crypto({}).unwrap_key(obj_key, body_key_meta)
obj_key, wrapped_body_key, body_iv)
exp_enc_body = encrypt(self.plaintext, body_key, body_iv) exp_enc_body = encrypt(self.plaintext, body_key, body_iv)
self.assertEqual(exp_enc_body, contents) self.assertEqual(exp_enc_body, contents)
# verify on disk user metadata # verify on disk user metadata