aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2017-10-02 10:03:20 +0800
committerAlex Gaynor <alex.gaynor@gmail.com>2017-10-01 22:03:20 -0400
commita397d75a1e091299d012035655bdc30376378b4c (patch)
tree6cc453b672db069abe64838ec3d4d990777f20fc
parentdd567cbf732d310e8a79aa05d7001c8639e9e6f3 (diff)
downloadcryptography-a397d75a1e091299d012035655bdc30376378b4c.tar.gz
cryptography-a397d75a1e091299d012035655bdc30376378b4c.tar.bz2
cryptography-a397d75a1e091299d012035655bdc30376378b4c.zip
Add support for AES XTS (#3900)
* Add support for AES XTS We drop the non-byte aligned test vectors because according to NIST http://csrc.nist.gov/groups/STM/cavp/documents/aes/XTSVS.pdf "An implementation may support a data unit length that is not a multiple of 8 bits." OpenSSL does not support this, so we can't use those test vectors. * fix docs and pep8 * docs fix * the spellchecker is so frustrating * add note about AES 192 for XTS (it's not supported) * docs work * enforce key length on ECB mode in AES as well (thanks XTS) * a few more words about why we exclude some test vectors for XTS
-rw-r--r--CHANGELOG.rst4
-rw-r--r--docs/hazmat/primitives/symmetric-encryption.rst41
-rw-r--r--docs/spelling_wordlist.txt2
-rw-r--r--src/cryptography/hazmat/backends/openssl/backend.py8
-rw-r--r--src/cryptography/hazmat/backends/openssl/ciphers.py2
-rw-r--r--src/cryptography/hazmat/primitives/ciphers/algorithms.py3
-rw-r--r--src/cryptography/hazmat/primitives/ciphers/modes.py59
-rw-r--r--tests/hazmat/primitives/test_aes.py36
-rw-r--r--tests/hazmat/primitives/test_ciphers.py24
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),