Repository: pyca/cryptography

Issue:
AES-XTS failing for plaintext less then 16 bytes
I am using version 2.6.1 (Python 2)
Encrypting plaintext less then 16 bytes in AES XTS mode throws the following exception:

> "_cryptography.exceptions.InternalError: Unknown OpenSSL error. This error is commonly encountered when another library is not cleaning up the OpenSSL error stack. If you are using cryptography with another library that uses OpenSSL try disabling it before reporting a bug. Otherwise please file an issue at https://github.com/pyca/cryptography/issues with information on how to reproduce this."

See code snippet below
Thanks

`from cryptography.hazmat.primitives.ciphers import (
            Cipher, algorithms, modes
    )
    from cryptography.hazmat.backends import default_backend

    limit = 10000
    failures =  []
    for i in range(limit):
        key = os.urandom(64)
        tweak = os.urandom(16)
        
        txtLen = i
        plaintext = os.urandom(txtLen)
        
        encryptor = Cipher(
            algorithms.AES(key),
            modes.XTS(tweak),
            backend=default_backend()
        ).encryptor()
        
        ciphertext = encryptor.update(plaintext) + encryptor.finalize()
        try:
            ciphertext = encryptor.update(plaintext) + encryptor.finalize()
        except Exception as e:
            if (not txtLen in failures):
                failures.append(txtLen)
            
        
    for i in failures:
        print i,
        `


Discussion:
XTS is defined (see: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38e.pdf) for encrypting any data that consists of one or more complete blocks (128-bit since the underlying AES is a 128-bit block cipher) + a single partial block. So byte sizes 1-15 are invalid for XTS. Ideally we would catch this, although arguably OpenSSL should error more usefully as well.

I'll leave this open for the moment since we don't want an InternalError to occur here. We do already track data_processed so we can raise an error in finalize for XTS on this.
Yeah, this should behave the same as CBC. I don't remember what exception
we raise (or whether we do it or OpenSSL does), but we should error cleanly.

On Mon, Mar 1, 2021 at 5:07 PM Paul Kehrer <notifications@github.com> wrote:

> XTS is defined (see:
> https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38e.pdf)
> for encrypting any data that consists of one or more complete blocks
> (128-bit since the underlying AES is a 128-bit block cipher) + a single
> partial block. So byte sizes 1-15 are invalid for XTS. Ideally we would
> catch this, although arguably OpenSSL should error more usefully as well.
>
> I'll leave this open for the moment since we don't want an InternalError
> to occur here. We do already track data_processed so we can raise an error
> in finalize for XTS on this.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/pyca/cryptography/issues/5885#issuecomment-788333837>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAAAGBD5BESGWRFCXZXAV7LTBQFYJANCNFSM4YL5I3LA>
> .
>


-- 
All that is necessary for evil to succeed is for good people to do nothing.

It doesn't behave exactly the same as CBC as any input size is fine sans padding *except* for less than one block, but I think you just mean it should raise a ValueError. I agree 😄 
Ah, I misread. But yes, that.

On Mon, Mar 1, 2021 at 5:52 PM Paul Kehrer <notifications@github.com> wrote:
>
> It doesn't behave exactly the same as CBC as any input size is fine sans padding except for less than one block, but I think you just mean it should raise a ValueError. I agree
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub, or unsubscribe.



-- 
All that is necessary for evil to succeed is for good people to do nothing.

Fixing this while maintaining our API is difficult. OpenSSL normally buffers up to block length on block aligned modes. However, XTS is only block aligned for the *first* block, and they do not buffer it. This results in the error occurring on `update` and not on `finalize`.

We can resolve this by buffering ourselves, but that's biting off a lot of additional complexity. We could also just document that `update` can also return a `ValueError` and expect callers to handle it. This would be a breaking change, but it only occurs in situations where we currently raise InternalError. I'm inclined to just allow ValueError to happen on update for XTS less than block length.
Yes, that sounds fine to me.

On Sun, Mar 21, 2021 at 6:31 PM Paul Kehrer ***@***.***>
wrote:

> Fixing this while maintaining our API is difficult. OpenSSL normally
> buffers up to block length on block aligned modes. However, XTS is only
> block aligned for the *first* block, and they do not buffer it. This
> results in the error occurring on update and not on finalize.
>
> We can resolve this by buffering ourselves, but that's biting off a lot of
> additional complexity. We could also just document that update can also
> return a ValueError and expect callers to handle it. This would be a
> breaking change, but it only occurs in situations where we currently raise
> InternalError. I'm inclined to just allow ValueError to happen on update
> for XTS less than block length.
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/pyca/cryptography/issues/5885#issuecomment-803671486>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAAAGBBYJVQUI5U226AQ3G3TEZXU3ANCNFSM4YL5I3LA>
> .
>


-- 
All that is necessary for evil to succeed is for good people to do nothing.


Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/ciphers.py b/src/cryptography/hazmat/backends/openssl/ciphers.py
--- a/src/cryptography/hazmat/backends/openssl/ciphers.py
+++ b/src/cryptography/hazmat/backends/openssl/ciphers.py
@@ -145,7 +145,13 @@ def update_into(self, data: bytes, buf) -> int:
             res = self._backend._lib.EVP_CipherUpdate(
                 self._ctx, outbuf, outlen, inbuf, inlen
             )
-            self._backend.openssl_assert(res != 0)
+            if res == 0 and isinstance(self._mode, modes.XTS):
+                raise ValueError(
+                    "In XTS mode you must supply at least a full block in the "
+                    "first update call. For AES this is 16 bytes."
+                )
+            else:
+                self._backend.openssl_assert(res != 0)
             data_processed += inlen
             total_out += outlen[0]
 


Test patch:
diff --git a/tests/hazmat/primitives/test_aes.py b/tests/hazmat/primitives/test_aes.py
--- a/tests/hazmat/primitives/test_aes.py
+++ b/tests/hazmat/primitives/test_aes.py
@@ -52,6 +52,14 @@ def test_xts_vectors(self, backend, subtests):
                 computed_pt = dec.update(ct) + dec.finalize()
                 assert computed_pt == pt
 
+    def test_xts_too_short(self):
+        key = b"thirty_two_byte_keys_are_great!!"
+        tweak = b"\x00" * 16
+        cipher = base.Cipher(algorithms.AES(key), modes.XTS(tweak))
+        enc = cipher.encryptor()
+        with pytest.raises(ValueError):
+            enc.update(b"0" * 15)
+
 
 @pytest.mark.supported(
     only_if=lambda backend: backend.cipher_supported(