diff options
-rw-r--r-- | CHANGELOG.rst | 5 | ||||
-rw-r--r-- | cryptography/__about__.py | 2 | ||||
-rw-r--r-- | cryptography/hazmat/backends/commoncrypto/backend.py | 281 | ||||
-rw-r--r-- | cryptography/hazmat/backends/commoncrypto/ciphers.py | 191 | ||||
-rw-r--r-- | cryptography/hazmat/backends/commoncrypto/hashes.py | 62 | ||||
-rw-r--r-- | cryptography/hazmat/backends/commoncrypto/hmac.py | 58 | ||||
-rw-r--r-- | cryptography/hazmat/backends/openssl/backend.py | 15 | ||||
-rw-r--r-- | cryptography/hazmat/bindings/commoncrypto/seckey.py | 1 | ||||
-rw-r--r-- | cryptography/hazmat/primitives/ciphers/modes.py | 9 | ||||
-rw-r--r-- | cryptography/hazmat/primitives/constant_time.py | 16 | ||||
-rw-r--r-- | docs/fernet.rst | 2 | ||||
-rw-r--r-- | docs/hazmat/primitives/symmetric-encryption.rst | 23 | ||||
-rw-r--r-- | tests/hazmat/backends/test_commoncrypto.py | 6 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_aes.py | 4 | ||||
-rw-r--r-- | tests/hazmat/primitives/utils.py | 13 |
15 files changed, 387 insertions, 301 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e057b636..13bc23f8 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,11 @@ Changelog .. note:: This version is not yet released and is under active development. +* **BACKWARDS INCOMPATIBLE:** + :class:`~cryptography.hazmat.primitives.ciphers.modes.GCM` no longer allows + truncation of tags by default. Previous versions of ``cryptography`` allowed + tags to be truncated by default, applications wishing to preserve this + behavior (not recommended) can pass the ``min_tag_length`` argument. * Added :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDFExpand`. * Added :class:`~cryptography.hazmat.primitives.ciphers.modes.CFB8` support for :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES` and diff --git a/cryptography/__about__.py b/cryptography/__about__.py index ee53902b..ccbcdfe8 100644 --- a/cryptography/__about__.py +++ b/cryptography/__about__.py @@ -28,4 +28,4 @@ __author__ = "The cryptography developers" __email__ = "cryptography-dev@python.org" __license__ = "Apache License, Version 2.0" -__copyright__ = "Copyright 2013-2014 %s" % __author__ +__copyright__ = "Copyright 2013-2014 {0}".format(__author__) diff --git a/cryptography/hazmat/backends/commoncrypto/backend.py b/cryptography/hazmat/backends/commoncrypto/backend.py index 41be11f9..7bab979f 100644 --- a/cryptography/hazmat/backends/commoncrypto/backend.py +++ b/cryptography/hazmat/backends/commoncrypto/backend.py @@ -16,14 +16,16 @@ from __future__ import absolute_import, division, print_function from collections import namedtuple from cryptography import utils -from cryptography.exceptions import ( - InternalError, InvalidTag, UnsupportedAlgorithm, _Reasons +from cryptography.exceptions import InternalError +from cryptography.hazmat.backends.commoncrypto.ciphers import ( + _CipherContext, _GCMCipherContext ) +from cryptography.hazmat.backends.commoncrypto.hashes import _HashContext +from cryptography.hazmat.backends.commoncrypto.hmac import _HMACContext from cryptography.hazmat.backends.interfaces import ( CipherBackend, HMACBackend, HashBackend, PBKDF2HMACBackend ) from cryptography.hazmat.bindings.commoncrypto.binding import Binding -from cryptography.hazmat.primitives import constant_time, interfaces from cryptography.hazmat.primitives.ciphers.algorithms import ( AES, ARC4, Blowfish, CAST5, TripleDES ) @@ -147,7 +149,7 @@ class Backend(object): buf, length ) - self._check_response(res) + self._check_cipher_response(res) return self._ffi.buffer(buf)[:] @@ -221,7 +223,7 @@ class Backend(object): self._lib.kCCModeRC4 ) - def _check_response(self, response): + def _check_cipher_response(self, response): if response == self._lib.kCCSuccess: return elif response == self._lib.kCCAlignmentError: @@ -237,266 +239,15 @@ class Backend(object): " Code: {0}.".format(response) ) - -def _release_cipher_ctx(ctx): - """ - Called by the garbage collector and used to safely dereference and - release the context. - """ - if ctx[0] != backend._ffi.NULL: - res = backend._lib.CCCryptorRelease(ctx[0]) - backend._check_response(res) - ctx[0] = backend._ffi.NULL - - -@utils.register_interface(interfaces.CipherContext) -class _CipherContext(object): - def __init__(self, backend, cipher, mode, operation): - self._backend = backend - self._cipher = cipher - self._mode = mode - self._operation = operation - # There is a bug in CommonCrypto where block ciphers do not raise - # kCCAlignmentError when finalizing if you supply non-block aligned - # data. To work around this we need to keep track of the block - # alignment ourselves, but only for alg+mode combos that require - # block alignment. OFB, CFB, and CTR make a block cipher algorithm - # into a stream cipher so we don't need to track them (and thus their - # block size is effectively 1 byte just like OpenSSL/CommonCrypto - # treat RC4 and other stream cipher block sizes). - # This bug has been filed as rdar://15589470 - self._bytes_processed = 0 - if (isinstance(cipher, interfaces.BlockCipherAlgorithm) and not - isinstance(mode, (OFB, CFB, CFB8, CTR))): - self._byte_block_size = cipher.block_size // 8 - else: - self._byte_block_size = 1 - - registry = self._backend._cipher_registry - try: - cipher_enum, mode_enum = registry[type(cipher), type(mode)] - except KeyError: - raise UnsupportedAlgorithm( - "cipher {0} in {1} mode is not supported " - "by this backend.".format( - cipher.name, mode.name if mode else mode), - _Reasons.UNSUPPORTED_CIPHER - ) - - ctx = self._backend._ffi.new("CCCryptorRef *") - ctx = self._backend._ffi.gc(ctx, _release_cipher_ctx) - - if isinstance(mode, interfaces.ModeWithInitializationVector): - iv_nonce = mode.initialization_vector - elif isinstance(mode, interfaces.ModeWithNonce): - iv_nonce = mode.nonce - else: - iv_nonce = self._backend._ffi.NULL - - if isinstance(mode, CTR): - mode_option = self._backend._lib.kCCModeOptionCTR_BE - else: - mode_option = 0 - - res = self._backend._lib.CCCryptorCreateWithMode( - operation, - mode_enum, cipher_enum, - self._backend._lib.ccNoPadding, iv_nonce, - cipher.key, len(cipher.key), - self._backend._ffi.NULL, 0, 0, mode_option, ctx) - self._backend._check_response(res) - - self._ctx = ctx - - def update(self, data): - # Count bytes processed to handle block alignment. - self._bytes_processed += len(data) - buf = self._backend._ffi.new( - "unsigned char[]", len(data) + self._byte_block_size - 1) - outlen = self._backend._ffi.new("size_t *") - res = self._backend._lib.CCCryptorUpdate( - self._ctx[0], data, len(data), buf, - len(data) + self._byte_block_size - 1, outlen) - self._backend._check_response(res) - return self._backend._ffi.buffer(buf)[:outlen[0]] - - def finalize(self): - # Raise error if block alignment is wrong. - if self._bytes_processed % self._byte_block_size: - raise ValueError( - "The length of the provided data is not a multiple of " - "the block length." - ) - buf = self._backend._ffi.new("unsigned char[]", self._byte_block_size) - outlen = self._backend._ffi.new("size_t *") - res = self._backend._lib.CCCryptorFinal( - self._ctx[0], buf, len(buf), outlen) - self._backend._check_response(res) - _release_cipher_ctx(self._ctx) - return self._backend._ffi.buffer(buf)[:outlen[0]] - - -@utils.register_interface(interfaces.AEADCipherContext) -@utils.register_interface(interfaces.AEADEncryptionContext) -class _GCMCipherContext(object): - def __init__(self, backend, cipher, mode, operation): - self._backend = backend - self._cipher = cipher - self._mode = mode - self._operation = operation - self._tag = None - - registry = self._backend._cipher_registry - try: - cipher_enum, mode_enum = registry[type(cipher), type(mode)] - except KeyError: - raise UnsupportedAlgorithm( - "cipher {0} in {1} mode is not supported " - "by this backend.".format( - cipher.name, mode.name if mode else mode), - _Reasons.UNSUPPORTED_CIPHER - ) - - ctx = self._backend._ffi.new("CCCryptorRef *") - ctx = self._backend._ffi.gc(ctx, _release_cipher_ctx) - - self._ctx = ctx - - res = self._backend._lib.CCCryptorCreateWithMode( - operation, - mode_enum, cipher_enum, - self._backend._lib.ccNoPadding, - self._backend._ffi.NULL, - cipher.key, len(cipher.key), - self._backend._ffi.NULL, 0, 0, 0, self._ctx) - self._backend._check_response(res) - - res = self._backend._lib.CCCryptorGCMAddIV( - self._ctx[0], - mode.initialization_vector, - len(mode.initialization_vector) - ) - self._backend._check_response(res) - - def update(self, data): - buf = self._backend._ffi.new("unsigned char[]", len(data)) - args = (self._ctx[0], data, len(data), buf) - if self._operation == self._backend._lib.kCCEncrypt: - res = self._backend._lib.CCCryptorGCMEncrypt(*args) - else: - res = self._backend._lib.CCCryptorGCMDecrypt(*args) - - self._backend._check_response(res) - return self._backend._ffi.buffer(buf)[:] - - def finalize(self): - tag_size = self._cipher.block_size // 8 - tag_buf = self._backend._ffi.new("unsigned char[]", tag_size) - tag_len = self._backend._ffi.new("size_t *", tag_size) - res = backend._lib.CCCryptorGCMFinal(self._ctx[0], tag_buf, tag_len) - self._backend._check_response(res) - _release_cipher_ctx(self._ctx) - self._tag = self._backend._ffi.buffer(tag_buf)[:] - if (self._operation == self._backend._lib.kCCDecrypt and - not constant_time.bytes_eq( - self._tag[:len(self._mode.tag)], self._mode.tag - )): - raise InvalidTag - return b"" - - def authenticate_additional_data(self, data): - res = self._backend._lib.CCCryptorGCMAddAAD( - self._ctx[0], data, len(data) - ) - self._backend._check_response(res) - - @property - def tag(self): - return self._tag - - -@utils.register_interface(interfaces.HashContext) -class _HashContext(object): - def __init__(self, backend, algorithm, ctx=None): - self.algorithm = algorithm - self._backend = backend - - if ctx is None: - try: - methods = self._backend._hash_mapping[self.algorithm.name] - except KeyError: - raise UnsupportedAlgorithm( - "{0} is not a supported hash on this backend.".format( - algorithm.name), - _Reasons.UNSUPPORTED_HASH - ) - ctx = self._backend._ffi.new(methods.ctx) - res = methods.hash_init(ctx) - assert res == 1 - - self._ctx = ctx - - def copy(self): - methods = self._backend._hash_mapping[self.algorithm.name] - new_ctx = self._backend._ffi.new(methods.ctx) - # CommonCrypto has no APIs for copying hashes, so we have to copy the - # underlying struct. - new_ctx[0] = self._ctx[0] - - return _HashContext(self._backend, self.algorithm, ctx=new_ctx) - - def update(self, data): - methods = self._backend._hash_mapping[self.algorithm.name] - res = methods.hash_update(self._ctx, data, len(data)) - assert res == 1 - - def finalize(self): - methods = self._backend._hash_mapping[self.algorithm.name] - buf = self._backend._ffi.new("unsigned char[]", - self.algorithm.digest_size) - res = methods.hash_final(buf, self._ctx) - assert res == 1 - return self._backend._ffi.buffer(buf)[:] - - -@utils.register_interface(interfaces.HashContext) -class _HMACContext(object): - def __init__(self, backend, key, algorithm, ctx=None): - self.algorithm = algorithm - self._backend = backend - if ctx is None: - ctx = self._backend._ffi.new("CCHmacContext *") - try: - alg = self._backend._supported_hmac_algorithms[algorithm.name] - except KeyError: - raise UnsupportedAlgorithm( - "{0} is not a supported HMAC hash on this backend.".format( - algorithm.name), - _Reasons.UNSUPPORTED_HASH - ) - - self._backend._lib.CCHmacInit(ctx, alg, key, len(key)) - - self._ctx = ctx - self._key = key - - def copy(self): - copied_ctx = self._backend._ffi.new("CCHmacContext *") - # CommonCrypto has no APIs for copying HMACs, so we have to copy the - # underlying struct. - copied_ctx[0] = self._ctx[0] - return _HMACContext( - self._backend, self._key, self.algorithm, ctx=copied_ctx - ) - - def update(self, data): - self._backend._lib.CCHmacUpdate(self._ctx, data, len(data)) - - def finalize(self): - buf = self._backend._ffi.new("unsigned char[]", - self.algorithm.digest_size) - self._backend._lib.CCHmacFinal(self._ctx, buf) - return self._backend._ffi.buffer(buf)[:] + def _release_cipher_ctx(self, ctx): + """ + Called by the garbage collector and used to safely dereference and + release the context. + """ + if ctx[0] != self._ffi.NULL: + res = self._lib.CCCryptorRelease(ctx[0]) + self._check_cipher_response(res) + ctx[0] = self._ffi.NULL backend = Backend() diff --git a/cryptography/hazmat/backends/commoncrypto/ciphers.py b/cryptography/hazmat/backends/commoncrypto/ciphers.py new file mode 100644 index 00000000..525500c8 --- /dev/null +++ b/cryptography/hazmat/backends/commoncrypto/ciphers.py @@ -0,0 +1,191 @@ +# 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. + +from __future__ import absolute_import, division, print_function + +from cryptography import utils +from cryptography.exceptions import ( + InvalidTag, UnsupportedAlgorithm, _Reasons +) +from cryptography.hazmat.primitives import constant_time, interfaces +from cryptography.hazmat.primitives.ciphers.modes import ( + CFB, CFB8, CTR, OFB +) + + +@utils.register_interface(interfaces.CipherContext) +class _CipherContext(object): + def __init__(self, backend, cipher, mode, operation): + self._backend = backend + self._cipher = cipher + self._mode = mode + self._operation = operation + # There is a bug in CommonCrypto where block ciphers do not raise + # kCCAlignmentError when finalizing if you supply non-block aligned + # data. To work around this we need to keep track of the block + # alignment ourselves, but only for alg+mode combos that require + # block alignment. OFB, CFB, and CTR make a block cipher algorithm + # into a stream cipher so we don't need to track them (and thus their + # block size is effectively 1 byte just like OpenSSL/CommonCrypto + # treat RC4 and other stream cipher block sizes). + # This bug has been filed as rdar://15589470 + self._bytes_processed = 0 + if (isinstance(cipher, interfaces.BlockCipherAlgorithm) and not + isinstance(mode, (OFB, CFB, CFB8, CTR))): + self._byte_block_size = cipher.block_size // 8 + else: + self._byte_block_size = 1 + + registry = self._backend._cipher_registry + try: + cipher_enum, mode_enum = registry[type(cipher), type(mode)] + except KeyError: + raise UnsupportedAlgorithm( + "cipher {0} in {1} mode is not supported " + "by this backend.".format( + cipher.name, mode.name if mode else mode), + _Reasons.UNSUPPORTED_CIPHER + ) + + ctx = self._backend._ffi.new("CCCryptorRef *") + ctx = self._backend._ffi.gc(ctx, self._backend._release_cipher_ctx) + + if isinstance(mode, interfaces.ModeWithInitializationVector): + iv_nonce = mode.initialization_vector + elif isinstance(mode, interfaces.ModeWithNonce): + iv_nonce = mode.nonce + else: + iv_nonce = self._backend._ffi.NULL + + if isinstance(mode, CTR): + mode_option = self._backend._lib.kCCModeOptionCTR_BE + else: + mode_option = 0 + + res = self._backend._lib.CCCryptorCreateWithMode( + operation, + mode_enum, cipher_enum, + self._backend._lib.ccNoPadding, iv_nonce, + cipher.key, len(cipher.key), + self._backend._ffi.NULL, 0, 0, mode_option, ctx) + self._backend._check_cipher_response(res) + + self._ctx = ctx + + def update(self, data): + # Count bytes processed to handle block alignment. + self._bytes_processed += len(data) + buf = self._backend._ffi.new( + "unsigned char[]", len(data) + self._byte_block_size - 1) + outlen = self._backend._ffi.new("size_t *") + res = self._backend._lib.CCCryptorUpdate( + self._ctx[0], data, len(data), buf, + len(data) + self._byte_block_size - 1, outlen) + self._backend._check_cipher_response(res) + return self._backend._ffi.buffer(buf)[:outlen[0]] + + def finalize(self): + # Raise error if block alignment is wrong. + if self._bytes_processed % self._byte_block_size: + raise ValueError( + "The length of the provided data is not a multiple of " + "the block length." + ) + buf = self._backend._ffi.new("unsigned char[]", self._byte_block_size) + outlen = self._backend._ffi.new("size_t *") + res = self._backend._lib.CCCryptorFinal( + self._ctx[0], buf, len(buf), outlen) + self._backend._check_cipher_response(res) + self._backend._release_cipher_ctx(self._ctx) + return self._backend._ffi.buffer(buf)[:outlen[0]] + + +@utils.register_interface(interfaces.AEADCipherContext) +@utils.register_interface(interfaces.AEADEncryptionContext) +class _GCMCipherContext(object): + def __init__(self, backend, cipher, mode, operation): + self._backend = backend + self._cipher = cipher + self._mode = mode + self._operation = operation + self._tag = None + + registry = self._backend._cipher_registry + try: + cipher_enum, mode_enum = registry[type(cipher), type(mode)] + except KeyError: + raise UnsupportedAlgorithm( + "cipher {0} in {1} mode is not supported " + "by this backend.".format( + cipher.name, mode.name if mode else mode), + _Reasons.UNSUPPORTED_CIPHER + ) + + ctx = self._backend._ffi.new("CCCryptorRef *") + ctx = self._backend._ffi.gc(ctx, self._backend._release_cipher_ctx) + + self._ctx = ctx + + res = self._backend._lib.CCCryptorCreateWithMode( + operation, + mode_enum, cipher_enum, + self._backend._lib.ccNoPadding, + self._backend._ffi.NULL, + cipher.key, len(cipher.key), + self._backend._ffi.NULL, 0, 0, 0, self._ctx) + self._backend._check_cipher_response(res) + + res = self._backend._lib.CCCryptorGCMAddIV( + self._ctx[0], + mode.initialization_vector, + len(mode.initialization_vector) + ) + self._backend._check_cipher_response(res) + + def update(self, data): + buf = self._backend._ffi.new("unsigned char[]", len(data)) + args = (self._ctx[0], data, len(data), buf) + if self._operation == self._backend._lib.kCCEncrypt: + res = self._backend._lib.CCCryptorGCMEncrypt(*args) + else: + res = self._backend._lib.CCCryptorGCMDecrypt(*args) + + self._backend._check_cipher_response(res) + return self._backend._ffi.buffer(buf)[:] + + def finalize(self): + tag_size = self._cipher.block_size // 8 + tag_buf = self._backend._ffi.new("unsigned char[]", tag_size) + tag_len = self._backend._ffi.new("size_t *", tag_size) + res = self._backend._lib.CCCryptorGCMFinal( + self._ctx[0], tag_buf, tag_len + ) + self._backend._check_cipher_response(res) + self._backend._release_cipher_ctx(self._ctx) + self._tag = self._backend._ffi.buffer(tag_buf)[:] + if (self._operation == self._backend._lib.kCCDecrypt and + not constant_time.bytes_eq( + self._tag[:len(self._mode.tag)], self._mode.tag + )): + raise InvalidTag + return b"" + + def authenticate_additional_data(self, data): + res = self._backend._lib.CCCryptorGCMAddAAD( + self._ctx[0], data, len(data) + ) + self._backend._check_cipher_response(res) + + @property + def tag(self): + return self._tag diff --git a/cryptography/hazmat/backends/commoncrypto/hashes.py b/cryptography/hazmat/backends/commoncrypto/hashes.py new file mode 100644 index 00000000..ebad7201 --- /dev/null +++ b/cryptography/hazmat/backends/commoncrypto/hashes.py @@ -0,0 +1,62 @@ +# 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. + +from __future__ import absolute_import, division, print_function + +from cryptography import utils +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.primitives import interfaces + + +@utils.register_interface(interfaces.HashContext) +class _HashContext(object): + def __init__(self, backend, algorithm, ctx=None): + self.algorithm = algorithm + self._backend = backend + + if ctx is None: + try: + methods = self._backend._hash_mapping[self.algorithm.name] + except KeyError: + raise UnsupportedAlgorithm( + "{0} is not a supported hash on this backend.".format( + algorithm.name), + _Reasons.UNSUPPORTED_HASH + ) + ctx = self._backend._ffi.new(methods.ctx) + res = methods.hash_init(ctx) + assert res == 1 + + self._ctx = ctx + + def copy(self): + methods = self._backend._hash_mapping[self.algorithm.name] + new_ctx = self._backend._ffi.new(methods.ctx) + # CommonCrypto has no APIs for copying hashes, so we have to copy the + # underlying struct. + new_ctx[0] = self._ctx[0] + + return _HashContext(self._backend, self.algorithm, ctx=new_ctx) + + def update(self, data): + methods = self._backend._hash_mapping[self.algorithm.name] + res = methods.hash_update(self._ctx, data, len(data)) + assert res == 1 + + def finalize(self): + methods = self._backend._hash_mapping[self.algorithm.name] + buf = self._backend._ffi.new("unsigned char[]", + self.algorithm.digest_size) + res = methods.hash_final(buf, self._ctx) + assert res == 1 + return self._backend._ffi.buffer(buf)[:] diff --git a/cryptography/hazmat/backends/commoncrypto/hmac.py b/cryptography/hazmat/backends/commoncrypto/hmac.py new file mode 100644 index 00000000..ec3a878b --- /dev/null +++ b/cryptography/hazmat/backends/commoncrypto/hmac.py @@ -0,0 +1,58 @@ +# 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. + +from __future__ import absolute_import, division, print_function + +from cryptography import utils +from cryptography.exceptions import UnsupportedAlgorithm, _Reasons +from cryptography.hazmat.primitives import interfaces + + +@utils.register_interface(interfaces.HashContext) +class _HMACContext(object): + def __init__(self, backend, key, algorithm, ctx=None): + self.algorithm = algorithm + self._backend = backend + if ctx is None: + ctx = self._backend._ffi.new("CCHmacContext *") + try: + alg = self._backend._supported_hmac_algorithms[algorithm.name] + except KeyError: + raise UnsupportedAlgorithm( + "{0} is not a supported HMAC hash on this backend.".format( + algorithm.name), + _Reasons.UNSUPPORTED_HASH + ) + + self._backend._lib.CCHmacInit(ctx, alg, key, len(key)) + + self._ctx = ctx + self._key = key + + def copy(self): + copied_ctx = self._backend._ffi.new("CCHmacContext *") + # CommonCrypto has no APIs for copying HMACs, so we have to copy the + # underlying struct. + copied_ctx[0] = self._ctx[0] + return _HMACContext( + self._backend, self._key, self.algorithm, ctx=copied_ctx + ) + + def update(self, data): + self._backend._lib.CCHmacUpdate(self._ctx, data, len(data)) + + def finalize(self): + buf = self._backend._ffi.new("unsigned char[]", + self.algorithm.digest_size) + self._backend._lib.CCHmacFinal(self._ctx, buf) + return self._backend._ffi.buffer(buf)[:] diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py index e8fc3a4d..4991177a 100644 --- a/cryptography/hazmat/backends/openssl/backend.py +++ b/cryptography/hazmat/backends/openssl/backend.py @@ -506,10 +506,10 @@ class Backend(object): ) def _rsa_cdata_from_private_key(self, private_key): - # Does not GC the RSA cdata. You *must* make sure it's freed - # correctly yourself! ctx = self._lib.RSA_new() assert ctx != self._ffi.NULL + ctx = self._ffi.gc(ctx, self._lib.RSA_free) + ctx.p = self._int_to_bn(private_key.p) ctx.q = self._int_to_bn(private_key.q) ctx.d = self._int_to_bn(private_key.d) @@ -524,11 +524,10 @@ class Backend(object): return ctx def _rsa_cdata_from_public_key(self, public_key): - # Does not GC the RSA cdata. You *must* make sure it's freed - # correctly yourself! - ctx = self._lib.RSA_new() assert ctx != self._ffi.NULL + ctx = self._ffi.gc(ctx, self._lib.RSA_free) + ctx.e = self._int_to_bn(public_key.e) ctx.n = self._int_to_bn(public_key.n) res = self._lib.RSA_blinding_on(ctx, self._ffi.NULL) @@ -544,7 +543,6 @@ class Backend(object): stacklevel=2 ) rsa_cdata = self._rsa_cdata_from_private_key(private_key) - rsa_cdata = self._ffi.gc(rsa_cdata, self._lib.RSA_free) key = _RSAPrivateKey(self, rsa_cdata) return _RSASignatureContext(self, key, padding, algorithm) @@ -557,7 +555,6 @@ class Backend(object): stacklevel=2 ) rsa_cdata = self._rsa_cdata_from_public_key(public_key) - rsa_cdata = self._ffi.gc(rsa_cdata, self._lib.RSA_free) key = _RSAPublicKey(self, rsa_cdata) return _RSAVerificationContext(self, key, signature, padding, algorithm) @@ -739,7 +736,6 @@ class Backend(object): stacklevel=2 ) rsa_cdata = self._rsa_cdata_from_private_key(private_key) - rsa_cdata = self._ffi.gc(rsa_cdata, self._lib.RSA_free) key = _RSAPrivateKey(self, rsa_cdata) return key.decrypt(ciphertext, padding) @@ -751,7 +747,6 @@ class Backend(object): stacklevel=2 ) rsa_cdata = self._rsa_cdata_from_public_key(public_key) - rsa_cdata = self._ffi.gc(rsa_cdata, self._lib.RSA_free) key = _RSAPublicKey(self, rsa_cdata) return key.encrypt(plaintext, padding) @@ -917,7 +912,7 @@ class Backend(object): Generate a new private key on the named curve. """ - if backend.elliptic_curve_supported(curve): + if self.elliptic_curve_supported(curve): curve_nid = self._elliptic_curve_to_nid(curve) ctx = self._lib.EC_KEY_new_by_curve_name(curve_nid) diff --git a/cryptography/hazmat/bindings/commoncrypto/seckey.py b/cryptography/hazmat/bindings/commoncrypto/seckey.py index 38aaece8..5e4b6dac 100644 --- a/cryptography/hazmat/bindings/commoncrypto/seckey.py +++ b/cryptography/hazmat/bindings/commoncrypto/seckey.py @@ -23,6 +23,7 @@ typedef ... *SecKeyRef; FUNCTIONS = """ OSStatus SecKeyGeneratePair(CFDictionaryRef, SecKeyRef *, SecKeyRef *); +size_t SecKeyGetBlockSize(SecKeyRef); """ MACROS = """ diff --git a/cryptography/hazmat/primitives/ciphers/modes.py b/cryptography/hazmat/primitives/ciphers/modes.py index e70a9db5..509b4de2 100644 --- a/cryptography/hazmat/primitives/ciphers/modes.py +++ b/cryptography/hazmat/primitives/ciphers/modes.py @@ -97,13 +97,16 @@ class CTR(object): class GCM(object): name = "GCM" - def __init__(self, initialization_vector, tag=None): + def __init__(self, initialization_vector, tag=None, min_tag_length=16): # len(initialization_vector) must in [1, 2 ** 64), but it's impossible # to actually construct a bytes object that large, so we don't check # for it - if tag is not None and len(tag) < 4: + if min_tag_length < 4: + raise ValueError("min_tag_length must be >= 4") + if tag is not None and len(tag) < min_tag_length: raise ValueError( - "Authentication tag must be 4 bytes or longer." + "Authentication tag must be {0} bytes or longer.".format( + min_tag_length) ) self.initialization_vector = initialization_vector diff --git a/cryptography/hazmat/primitives/constant_time.py b/cryptography/hazmat/primitives/constant_time.py index 4547da13..9789851a 100644 --- a/cryptography/hazmat/primitives/constant_time.py +++ b/cryptography/hazmat/primitives/constant_time.py @@ -13,6 +13,7 @@ from __future__ import absolute_import, division, print_function +import hmac import sys import cffi @@ -53,9 +54,18 @@ _lib = _ffi.verify( ext_package="cryptography", ) +if hasattr(hmac, "compare_digest"): + def bytes_eq(a, b): + if not isinstance(a, bytes) or not isinstance(b, bytes): + raise TypeError("a and b must be bytes.") + + return hmac.compare_digest(a, b) -def bytes_eq(a, b): - if not isinstance(a, bytes) or not isinstance(b, bytes): +else: + def bytes_eq(a, b): + if not isinstance(a, bytes) or not isinstance(b, bytes): raise TypeError("a and b must be bytes.") - return _lib.Cryptography_constant_time_bytes_eq(a, len(a), b, len(b)) == 1 + return _lib.Cryptography_constant_time_bytes_eq( + a, len(a), b, len(b) + ) == 1 diff --git a/docs/fernet.rst b/docs/fernet.rst index 1c4918ad..4b713a54 100644 --- a/docs/fernet.rst +++ b/docs/fernet.rst @@ -83,7 +83,7 @@ Specifically it uses: * :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES` in :class:`~cryptography.hazmat.primitives.ciphers.modes.CBC` mode with a 128-bit key for encryption; using - :class:`~cryptography.hazmat.primitives.ciphers.PKCS7` padding. + :class:`~cryptography.hazmat.primitives.padding.PKCS7` padding. * :class:`~cryptography.hazmat.primitives.hmac.HMAC` using :class:`~cryptography.hazmat.primitives.hashes.SHA256` for authentication. * Initialization vectors are generated using ``os.urandom()``. diff --git a/docs/hazmat/primitives/symmetric-encryption.rst b/docs/hazmat/primitives/symmetric-encryption.rst index abc2b076..586285b7 100644 --- a/docs/hazmat/primitives/symmetric-encryption.rst +++ b/docs/hazmat/primitives/symmetric-encryption.rst @@ -288,7 +288,7 @@ Modes Must be the same number of bytes as the ``block_size`` of the cipher. Do not reuse an ``initialization_vector`` with a given ``key``. -.. class:: GCM(initialization_vector, tag=None) +.. class:: GCM(initialization_vector, tag=None, min_tag_length=16) .. danger:: @@ -318,13 +318,23 @@ Modes You can shorten a tag by truncating it to the desired length but this is **not recommended** as it lowers the security margins of the authentication (`NIST SP-800-38D`_ recommends 96-bits or greater). - If you must shorten the tag the minimum allowed length is 4 bytes - (32-bits). Applications **must** verify the tag is the expected length - to guarantee the expected security margin. + Applications wishing to allow truncation must pass the + ``min_tag_length`` parameter. + + .. versionchanged:: 0.5 + + The ``min_tag_length`` parameter was added in ``0.5``, previously + truncation down to ``4`` bytes was always allowed. :param bytes tag: The tag bytes to verify during decryption. When encrypting this must be ``None``. + :param bytes min_tag_length: The minimum length ``tag`` must be. By default + this is ``16``, meaning tag truncation is not allowed. Allowing tag + truncation is strongly discouraged for most applications. + + :raises ValueError: This is raised if ``len(tag) < min_tag_length``. + .. testcode:: import os @@ -356,11 +366,6 @@ Modes return (iv, ciphertext, encryptor.tag) def decrypt(key, associated_data, iv, ciphertext, tag): - if len(tag) != 16: - raise ValueError( - "tag must be 16 bytes -- truncation not supported" - ) - # Construct a Cipher object, with the key, iv, and additionally the # GCM tag used for authenticating the message. decryptor = Cipher( diff --git a/tests/hazmat/backends/test_commoncrypto.py b/tests/hazmat/backends/test_commoncrypto.py index 7c703f67..e2c6f4a0 100644 --- a/tests/hazmat/backends/test_commoncrypto.py +++ b/tests/hazmat/backends/test_commoncrypto.py @@ -51,13 +51,13 @@ class TestCommonCrypto(object): from cryptography.hazmat.backends.commoncrypto.backend import backend with pytest.raises(ValueError): - backend._check_response(backend._lib.kCCAlignmentError) + backend._check_cipher_response(backend._lib.kCCAlignmentError) with pytest.raises(InternalError): - backend._check_response(backend._lib.kCCMemoryFailure) + backend._check_cipher_response(backend._lib.kCCMemoryFailure) with pytest.raises(InternalError): - backend._check_response(backend._lib.kCCDecodeError) + backend._check_cipher_response(backend._lib.kCCDecodeError) def test_nonexistent_aead_cipher(self): from cryptography.hazmat.backends.commoncrypto.backend import Backend diff --git a/tests/hazmat/primitives/test_aes.py b/tests/hazmat/primitives/test_aes.py index 173075d6..5bde7d3c 100644 --- a/tests/hazmat/primitives/test_aes.py +++ b/tests/hazmat/primitives/test_aes.py @@ -225,6 +225,6 @@ class TestAESModeGCM(object): "gcmEncryptExtIV192.rsp", "gcmEncryptExtIV256.rsp", ], - lambda key: algorithms.AES(key), - lambda iv, tag: modes.GCM(iv, tag), + algorithms.AES, + modes.GCM, ) diff --git a/tests/hazmat/primitives/utils.py b/tests/hazmat/primitives/utils.py index 49b73f01..6428b03e 100644 --- a/tests/hazmat/primitives/utils.py +++ b/tests/hazmat/primitives/utils.py @@ -90,7 +90,8 @@ def aead_test(backend, cipher_factory, mode_factory, params): cipher = Cipher( cipher_factory(binascii.unhexlify(params["key"])), mode_factory(binascii.unhexlify(params["iv"]), - binascii.unhexlify(params["tag"])), + binascii.unhexlify(params["tag"]), + len(binascii.unhexlify(params["tag"]))), backend ) decryptor = cipher.decryptor() @@ -108,12 +109,13 @@ def aead_test(backend, cipher_factory, mode_factory, params): encryptor.authenticate_additional_data(binascii.unhexlify(aad)) actual_ciphertext = encryptor.update(binascii.unhexlify(plaintext)) actual_ciphertext += encryptor.finalize() - tag_len = len(params["tag"]) - assert binascii.hexlify(encryptor.tag)[:tag_len] == params["tag"] + tag_len = len(binascii.unhexlify(params["tag"])) + assert binascii.hexlify(encryptor.tag[:tag_len]) == params["tag"] cipher = Cipher( cipher_factory(binascii.unhexlify(params["key"])), mode_factory(binascii.unhexlify(params["iv"]), - binascii.unhexlify(params["tag"])), + binascii.unhexlify(params["tag"]), + min_tag_length=tag_len), backend ) decryptor = cipher.decryptor() @@ -309,6 +311,9 @@ def aead_tag_exception_test(backend, cipher_factory, mode_factory): with pytest.raises(ValueError): mode_factory(binascii.unhexlify(b"0" * 24), b"000") + with pytest.raises(ValueError): + mode_factory(binascii.unhexlify(b"0" * 24), b"000000", 2) + cipher = Cipher( cipher_factory(binascii.unhexlify(b"0" * 32)), mode_factory(binascii.unhexlify(b"0" * 24), b"0" * 16), |