Repository: pyca/cryptography

Issue:
Data loss with some chunk sizes
``` pycon
>>> from cryptography.primitives.block import BlockCipher, ciphers, modes
>>> cipher = BlockCipher(ciphers.AES(b"\x00" * 16), modes.CBC(b"\x00" * 16))
<cryptography.primitives.block.base.BlockCipher object at 0x1066b2f90>
>>> cipher.encrypt(b"a")
''
>>> cipher.encrypt(b"a" * 63)
'\x92l\x0f\xea\x05\xaf\xd6_Y1\xd2\x9a\x9ck?\x9c\x04 73\xc9e\x9b*\x8b\xe6\xff\xb2\x83\x80\xb3z\x8c\x96\xcc\x16=^\x95\x8f\xb5\xfa6\xd77\xbfi\xaf\x97\xfa\x1f\x1fhrd\x16d\xcf\xb0\xd8V\x87\x1e'
>>> len(_)
63
>>> cipher.finalize()
''
```

Data loss with some chunk sizes
``` pycon
>>> from cryptography.primitives.block import BlockCipher, ciphers, modes
>>> cipher = BlockCipher(ciphers.AES(b"\x00" * 16), modes.CBC(b"\x00" * 16))
<cryptography.primitives.block.base.BlockCipher object at 0x1066b2f90>
>>> cipher.encrypt(b"a")
''
>>> cipher.encrypt(b"a" * 63)
'\x92l\x0f\xea\x05\xaf\xd6_Y1\xd2\x9a\x9ck?\x9c\x04 73\xc9e\x9b*\x8b\xe6\xff\xb2\x83\x80\xb3z\x8c\x96\xcc\x16=^\x95\x8f\xb5\xfa6\xd77\xbfi\xaf\x97\xfa\x1f\x1fhrd\x16d\xcf\xb0\xd8V\x87\x1e'
>>> len(_)
63
>>> cipher.finalize()
''
```



Discussion:
Looks like this bug is an error in our buffer sizing. When encryptupdate or decryptupdate write to the buffer the amount of data depends on the block alignment of the encrypted data. Output can be between zero and len(data) + block_size - 1. We only size for len(data).

I'll prep a patch.

Looks like this bug is an error in our buffer sizing. When encryptupdate or decryptupdate write to the buffer the amount of data depends on the block alignment of the encrypted data. Output can be between zero and len(data) + block_size - 1. We only size for len(data).

I'll prep a patch.


Resolving patch:
diff --git a/cryptography/bindings/openssl/api.py b/cryptography/bindings/openssl/api.py
--- a/cryptography/bindings/openssl/api.py
+++ b/cryptography/bindings/openssl/api.py
@@ -126,7 +126,8 @@ def create_block_cipher_context(self, cipher, mode):
         return ctx
 
     def update_encrypt_context(self, ctx, plaintext):
-        buf = self.ffi.new("unsigned char[]", len(plaintext))
+        block_size = self.lib.EVP_CIPHER_CTX_block_size(ctx)
+        buf = self.ffi.new("unsigned char[]", len(plaintext) + block_size - 1)
         outlen = self.ffi.new("int *")
         res = self.lib.EVP_EncryptUpdate(
             ctx, buf, outlen, plaintext, len(plaintext)
@@ -135,8 +136,7 @@ def update_encrypt_context(self, ctx, plaintext):
         return self.ffi.buffer(buf)[:outlen[0]]
 
     def finalize_encrypt_context(self, ctx):
-        cipher = self.lib.EVP_CIPHER_CTX_cipher(ctx)
-        block_size = self.lib.EVP_CIPHER_block_size(cipher)
+        block_size = self.lib.EVP_CIPHER_CTX_block_size(ctx)
         buf = self.ffi.new("unsigned char[]", block_size)
         outlen = self.ffi.new("int *")
         res = self.lib.EVP_EncryptFinal_ex(ctx, buf, outlen)
diff --git a/cryptography/bindings/openssl/evp.py b/cryptography/bindings/openssl/evp.py
--- a/cryptography/bindings/openssl/evp.py
+++ b/cryptography/bindings/openssl/evp.py
@@ -76,4 +76,5 @@
 MACROS = """
 int EVP_PKEY_assign_RSA(EVP_PKEY *, RSA *);
 int EVP_PKEY_assign_DSA(EVP_PKEY *, DSA *);
+int EVP_CIPHER_CTX_block_size(const EVP_CIPHER_CTX *);
 """


Test patch:
diff --git a/tests/primitives/test_block.py b/tests/primitives/test_block.py
--- a/tests/primitives/test_block.py
+++ b/tests/primitives/test_block.py
@@ -63,3 +63,14 @@ def test_finalize_with_invalid_operation(self, api):
 
         with pytest.raises(ValueError):
             cipher.finalize()
+
+    def test_unaligned_block_encryption(self, api):
+        cipher = BlockCipher(
+            ciphers.AES(binascii.unhexlify(b"0" * 32)),
+            modes.ECB(),
+            api
+        )
+        ct = cipher.encrypt(b"a" * 15)
+        assert ct == b""
+        ct += cipher.encrypt(b"a" * 65)
+        assert len(ct) == 80