aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.rst5
-rw-r--r--cryptography/__about__.py2
-rw-r--r--cryptography/hazmat/backends/commoncrypto/backend.py281
-rw-r--r--cryptography/hazmat/backends/commoncrypto/ciphers.py191
-rw-r--r--cryptography/hazmat/backends/commoncrypto/hashes.py62
-rw-r--r--cryptography/hazmat/backends/commoncrypto/hmac.py58
-rw-r--r--cryptography/hazmat/backends/openssl/backend.py15
-rw-r--r--cryptography/hazmat/bindings/commoncrypto/seckey.py1
-rw-r--r--cryptography/hazmat/primitives/ciphers/modes.py9
-rw-r--r--cryptography/hazmat/primitives/constant_time.py16
-rw-r--r--docs/fernet.rst2
-rw-r--r--docs/hazmat/primitives/symmetric-encryption.rst23
-rw-r--r--tests/hazmat/backends/test_commoncrypto.py6
-rw-r--r--tests/hazmat/primitives/test_aes.py4
-rw-r--r--tests/hazmat/primitives/utils.py13
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),