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
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 81aab1b..a4441b8 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -45,6 +45,9 @@
 * 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 @@
   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 10a349b..2635e75 100644
--- a/docs/hazmat/primitives/symmetric-encryption.rst
+++ b/docs/hazmat/primitives/symmetric-encryption.rst
@@ -469,6 +469,32 @@
 
         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 @@
         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 @@
 .. _`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 5eb896e..f0cfc88 100644
--- a/docs/spelling_wordlist.txt
+++ b/docs/spelling_wordlist.txt
@@ -11,6 +11,7 @@
 Capitan
 Changelog
 ciphertext
+codebook
 committer
 committers
 conda
@@ -99,3 +100,4 @@
 wildcard
 WoSign
 Xcode
+XEX
diff --git a/src/cryptography/hazmat/backends/openssl/backend.py b/src/cryptography/hazmat/backends/openssl/backend.py
index 2cbfca2..6abf4ec 100644
--- a/src/cryptography/hazmat/backends/openssl/backend.py
+++ b/src/cryptography/hazmat/backends/openssl/backend.py
@@ -61,7 +61,7 @@
     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 @@
             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 @@
         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 dfb33a0..8e55e28 100644
--- a/src/cryptography/hazmat/backends/openssl/ciphers.py
+++ b/src/cryptography/hazmat/backends/openssl/ciphers.py
@@ -57,6 +57,8 @@
 
         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 6e5eb31..99a837e 100644
--- a/src/cryptography/hazmat/primitives/ciphers/algorithms.py
+++ b/src/cryptography/hazmat/primitives/ciphers/algorithms.py
@@ -25,7 +25,8 @@
 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 54670b7..598dfaa 100644
--- a/src/cryptography/hazmat/primitives/ciphers/modes.py
+++ b/src/cryptography/hazmat/primitives/ciphers/modes.py
@@ -37,6 +37,15 @@
 
 
 @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 @@
         """
 
 
+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_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 @@
         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 @@
         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 @@
         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 @@
         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 @@
     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 @@
     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 a6b1e5f..a2a2988 100644
--- a/tests/hazmat/primitives/test_aes.py
+++ b/tests/hazmat/primitives/test_aes.py
@@ -12,12 +12,46 @@
 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 f1718c0..2f58c9f 100644
--- a/tests/hazmat/primitives/test_ciphers.py
+++ b/tests/hazmat/primitives/test_ciphers.py
@@ -37,6 +37,30 @@
             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),