diff options
-rw-r--r-- | CHANGELOG.rst | 4 | ||||
-rw-r--r-- | docs/hazmat/primitives/symmetric-encryption.rst | 41 | ||||
-rw-r--r-- | docs/spelling_wordlist.txt | 2 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/backend.py | 8 | ||||
-rw-r--r-- | src/cryptography/hazmat/backends/openssl/ciphers.py | 2 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/ciphers/algorithms.py | 3 | ||||
-rw-r--r-- | src/cryptography/hazmat/primitives/ciphers/modes.py | 59 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_aes.py | 36 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_ciphers.py | 24 |
9 files changed, 168 insertions, 11 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 81aab1ba..a4441b85 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -45,6 +45,9 @@ Changelog * Support :class:`~cryptography.hazmat.primitives.hashes.BLAKE2b` and :class:`~cryptography.hazmat.primitives.hashes.BLAKE2s` with :class:`~cryptography.hazmat.primitives.hmac.HMAC`. +* Added support for + :class:`~cryptography.hazmat.primitives.ciphers.modes.XTS` mode for + AES. * Added support for using labels with :class:`~cryptography.hazmat.primitives.asymmetric.padding.OAEP` when using OpenSSL 1.0.2 or greater. @@ -56,7 +59,6 @@ Changelog certificates. * Add support for the :class:`~cryptography.x509.FreshestCRL` extension. - .. _v2-0-3: 2.0.3 - 2017-08-03 diff --git a/docs/hazmat/primitives/symmetric-encryption.rst b/docs/hazmat/primitives/symmetric-encryption.rst index 10a349b1..2635e753 100644 --- a/docs/hazmat/primitives/symmetric-encryption.rst +++ b/docs/hazmat/primitives/symmetric-encryption.rst @@ -469,6 +469,32 @@ Modes a secret message! +.. class:: XTS(tweak) + + .. versionadded:: 2.1 + + .. warning:: + + XTS mode is meant for disk encryption and should not be used in other + contexts. ``cryptography`` only supports XTS mode with + :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES`. + + .. note:: + + AES XTS keys are double length. This means that to do AES-128 + encryption in XTS mode you need a 256-bit key. Similarly, AES-256 + requires passing a 512-bit key. AES 192 is not supported in XTS mode. + + XTS (XEX-based tweaked-codebook mode with ciphertext stealing) is a mode + of operation for the AES block cipher that is used for `disk encryption`_. + + **This mode does not require padding.** + + :param bytes tweak: The tweak is a 16 byte value typically derived from + something like the disk sector number. A given ``(tweak, key)`` pair + should not be reused, although doing so is less catastrophic than + in CTR mode. + Insecure modes -------------- @@ -744,6 +770,20 @@ Interfaces used by the symmetric cipher modes described in Exact requirements of the tag are described by the documentation of individual modes. + +.. class:: ModeWithTweak + + .. versionadded:: 2.1 + + A cipher mode with a tweak. + + .. attribute:: tweak + + :type: bytes + + Exact requirements of the tweak are described by the documentation of + individual modes. + Exceptions ~~~~~~~~~~ @@ -766,3 +806,4 @@ Exceptions .. _`significant patterns in the output`: https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Electronic_Codebook_.28ECB.29 .. _`International Data Encryption Algorithm`: https://en.wikipedia.org/wiki/International_Data_Encryption_Algorithm .. _`OpenPGP`: http://openpgp.org +.. _`disk encryption`: https://en.wikipedia.org/wiki/Disk_encryption_theory#XTS diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt index 5eb896e3..f0cfc88e 100644 --- a/docs/spelling_wordlist.txt +++ b/docs/spelling_wordlist.txt @@ -11,6 +11,7 @@ Botan Capitan Changelog ciphertext +codebook committer committers conda @@ -99,3 +100,4 @@ Verisign wildcard WoSign Xcode +XEX diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py index 2cbfca2c..6abf4ecc 100644 --- a/src/cryptography/hazmat/backends/openssl/backend.py +++ b/src/cryptography/hazmat/backends/openssl/backend.py @@ -61,7 +61,7 @@ from cryptography.hazmat.primitives.ciphers.algorithms import ( AES, ARC4, Blowfish, CAST5, Camellia, ChaCha20, IDEA, SEED, TripleDES ) from cryptography.hazmat.primitives.ciphers.modes import ( - CBC, CFB, CFB8, CTR, ECB, GCM, OFB + CBC, CFB, CFB8, CTR, ECB, GCM, OFB, XTS ) from cryptography.hazmat.primitives.kdf import scrypt @@ -263,6 +263,7 @@ class Backend(object): type(None), GetCipherByName("chacha20") ) + self.register_cipher_adapter(AES, XTS, _get_xts_cipher) def create_symmetric_encryption_ctx(self, cipher, mode): return _CipherContext(self, cipher, mode, _CipherContext._ENCRYPT) @@ -1961,4 +1962,9 @@ class GetCipherByName(object): return backend._lib.EVP_get_cipherbyname(cipher_name.encode("ascii")) +def _get_xts_cipher(backend, cipher, mode): + cipher_name = "aes-{0}-xts".format(cipher.key_size // 2) + return backend._lib.EVP_get_cipherbyname(cipher_name.encode("ascii")) + + backend = Backend() diff --git a/src/cryptography/hazmat/backends/openssl/ciphers.py b/src/cryptography/hazmat/backends/openssl/ciphers.py index dfb33a07..8e55e28b 100644 --- a/src/cryptography/hazmat/backends/openssl/ciphers.py +++ b/src/cryptography/hazmat/backends/openssl/ciphers.py @@ -57,6 +57,8 @@ class _CipherContext(object): if isinstance(mode, modes.ModeWithInitializationVector): iv_nonce = mode.initialization_vector + elif isinstance(mode, modes.ModeWithTweak): + iv_nonce = mode.tweak elif isinstance(mode, modes.ModeWithNonce): iv_nonce = mode.nonce elif isinstance(cipher, modes.ModeWithNonce): diff --git a/src/cryptography/hazmat/primitives/ciphers/algorithms.py b/src/cryptography/hazmat/primitives/ciphers/algorithms.py index 6e5eb313..99a837e4 100644 --- a/src/cryptography/hazmat/primitives/ciphers/algorithms.py +++ b/src/cryptography/hazmat/primitives/ciphers/algorithms.py @@ -25,7 +25,8 @@ def _verify_key_size(algorithm, key): class AES(object): name = "AES" block_size = 128 - key_sizes = frozenset([128, 192, 256]) + # 512 added to support AES-256-XTS, which uses 512-bit keys + key_sizes = frozenset([128, 192, 256, 512]) def __init__(self, key): self.key = _verify_key_size(self, key) diff --git a/src/cryptography/hazmat/primitives/ciphers/modes.py b/src/cryptography/hazmat/primitives/ciphers/modes.py index 54670b7f..598dfaa4 100644 --- a/src/cryptography/hazmat/primitives/ciphers/modes.py +++ b/src/cryptography/hazmat/primitives/ciphers/modes.py @@ -37,6 +37,15 @@ class ModeWithInitializationVector(object): @six.add_metaclass(abc.ABCMeta) +class ModeWithTweak(object): + @abc.abstractproperty + def tweak(self): + """ + The value of the tweak for this mode as bytes. + """ + + +@six.add_metaclass(abc.ABCMeta) class ModeWithNonce(object): @abc.abstractproperty def nonce(self): @@ -54,6 +63,13 @@ class ModeWithAuthenticationTag(object): """ +def _check_aes_key_length(self, algorithm): + if algorithm.key_size > 256 and algorithm.name == "AES": + raise ValueError( + "Only 128, 192, and 256 bit keys are allowed for this AES mode" + ) + + def _check_iv_length(self, algorithm): if len(self.initialization_vector) * 8 != algorithm.block_size: raise ValueError("Invalid IV size ({0}) for {1}.".format( @@ -61,6 +77,11 @@ def _check_iv_length(self, algorithm): )) +def _check_iv_and_key_length(self, algorithm): + _check_aes_key_length(self, algorithm) + _check_iv_length(self, algorithm) + + @utils.register_interface(Mode) @utils.register_interface(ModeWithInitializationVector) class CBC(object): @@ -73,15 +94,38 @@ class CBC(object): self._initialization_vector = initialization_vector initialization_vector = utils.read_only_property("_initialization_vector") - validate_for_algorithm = _check_iv_length + validate_for_algorithm = _check_iv_and_key_length + + +@utils.register_interface(Mode) +@utils.register_interface(ModeWithTweak) +class XTS(object): + name = "XTS" + + def __init__(self, tweak): + if not isinstance(tweak, bytes): + raise TypeError("tweak must be bytes") + + if len(tweak) != 16: + raise ValueError("tweak must be 128-bits (16 bytes)") + + self._tweak = tweak + + tweak = utils.read_only_property("_tweak") + + def validate_for_algorithm(self, algorithm): + if algorithm.key_size not in (256, 512): + raise ValueError( + "The XTS specification requires a 256-bit key for AES-128-XTS" + " and 512-bit key for AES-256-XTS" + ) @utils.register_interface(Mode) class ECB(object): name = "ECB" - def validate_for_algorithm(self, algorithm): - pass + validate_for_algorithm = _check_aes_key_length @utils.register_interface(Mode) @@ -96,7 +140,7 @@ class OFB(object): self._initialization_vector = initialization_vector initialization_vector = utils.read_only_property("_initialization_vector") - validate_for_algorithm = _check_iv_length + validate_for_algorithm = _check_iv_and_key_length @utils.register_interface(Mode) @@ -111,7 +155,7 @@ class CFB(object): self._initialization_vector = initialization_vector initialization_vector = utils.read_only_property("_initialization_vector") - validate_for_algorithm = _check_iv_length + validate_for_algorithm = _check_iv_and_key_length @utils.register_interface(Mode) @@ -126,7 +170,7 @@ class CFB8(object): self._initialization_vector = initialization_vector initialization_vector = utils.read_only_property("_initialization_vector") - validate_for_algorithm = _check_iv_length + validate_for_algorithm = _check_iv_and_key_length @utils.register_interface(Mode) @@ -143,6 +187,7 @@ class CTR(object): nonce = utils.read_only_property("_nonce") def validate_for_algorithm(self, algorithm): + _check_aes_key_length(self, algorithm) if len(self.nonce) * 8 != algorithm.block_size: raise ValueError("Invalid nonce size ({0}) for {1}.".format( len(self.nonce), self.name @@ -180,4 +225,4 @@ class GCM(object): initialization_vector = utils.read_only_property("_initialization_vector") def validate_for_algorithm(self, algorithm): - pass + _check_aes_key_length(self, algorithm) diff --git a/tests/hazmat/primitives/test_aes.py b/tests/hazmat/primitives/test_aes.py index a6b1e5f2..a2a29881 100644 --- a/tests/hazmat/primitives/test_aes.py +++ b/tests/hazmat/primitives/test_aes.py @@ -12,12 +12,46 @@ import pytest from cryptography.hazmat.backends.interfaces import CipherBackend from cryptography.hazmat.primitives.ciphers import algorithms, base, modes -from .utils import generate_aead_test, generate_encrypt_test +from .utils import _load_all_params, generate_aead_test, generate_encrypt_test from ...utils import load_nist_vectors @pytest.mark.supported( only_if=lambda backend: backend.cipher_supported( + algorithms.AES(b"\x00" * 32), modes.XTS(b"\x00" * 16) + ), + skip_message="Does not support AES XTS", +) +@pytest.mark.requires_backend_interface(interface=CipherBackend) +class TestAESModeXTS(object): + @pytest.mark.parametrize( + "vector", + # This list comprehension excludes any vector that does not have a + # data unit length that is divisible by 8. The NIST vectors include + # tests for implementations that support encryption of data that is + # not divisible modulo 8, but OpenSSL is not such an implementation. + [x for x in _load_all_params( + os.path.join("ciphers", "AES", "XTS", "tweak-128hexstr"), + ["XTSGenAES128.rsp", "XTSGenAES256.rsp"], + load_nist_vectors + ) if int(x["dataunitlen"]) / 8.0 == int(x["dataunitlen"]) // 8] + ) + def test_xts_vectors(self, vector, backend): + key = binascii.unhexlify(vector["key"]) + tweak = binascii.unhexlify(vector["i"]) + pt = binascii.unhexlify(vector["pt"]) + ct = binascii.unhexlify(vector["ct"]) + cipher = base.Cipher(algorithms.AES(key), modes.XTS(tweak), backend) + enc = cipher.encryptor() + computed_ct = enc.update(pt) + enc.finalize() + assert computed_ct == ct + dec = cipher.decryptor() + computed_pt = dec.update(ct) + dec.finalize() + assert computed_pt == pt + + +@pytest.mark.supported( + only_if=lambda backend: backend.cipher_supported( algorithms.AES(b"\x00" * 16), modes.CBC(b"\x00" * 16) ), skip_message="Does not support AES CBC", diff --git a/tests/hazmat/primitives/test_ciphers.py b/tests/hazmat/primitives/test_ciphers.py index f1718c07..2f58c9fc 100644 --- a/tests/hazmat/primitives/test_ciphers.py +++ b/tests/hazmat/primitives/test_ciphers.py @@ -37,6 +37,30 @@ class TestAES(object): AES(binascii.unhexlify(b"0" * 12)) +class TestAESXTS(object): + @pytest.mark.requires_backend_interface(interface=CipherBackend) + @pytest.mark.parametrize( + "mode", + (modes.CBC, modes.CTR, modes.CFB, modes.CFB8, modes.OFB) + ) + def test_invalid_key_size_with_mode(self, mode, backend): + with pytest.raises(ValueError): + ciphers.Cipher(AES(b"0" * 64), mode(b"0" * 16), backend) + + def test_xts_tweak_not_bytes(self): + with pytest.raises(TypeError): + modes.XTS(32) + + def test_xts_tweak_too_small(self): + with pytest.raises(ValueError): + modes.XTS(b"0") + + @pytest.mark.requires_backend_interface(interface=CipherBackend) + def test_xts_wrong_key_size(self, backend): + with pytest.raises(ValueError): + ciphers.Cipher(AES(b"0" * 16), modes.XTS(b"0" * 16), backend) + + class TestCamellia(object): @pytest.mark.parametrize(("key", "keysize"), [ (b"0" * 32, 128), |