Repository: pyca/cryptography

Issue:
data size limits are not documented for encrypt method on AESGCM
Using the AESGCM primitive, a OverflowError can occur.

Steps to reproduce:

```
import os
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
buf = b'x' * (2**32 + 1)
lend = default_backend()
iv = os.urandom(16)
key = os.urandom(32)
encryptor = AESGCM(key)
byts = encryptor.encrypt(iv, buf, None)
```
Results in the following exception:
```
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/user/.pyenv/versions/somepyenv/lib/python3.6/site-packages/cryptography/hazmat/primitives/ciphers/aead.py", line 147, in encrypt
    backend, self, nonce, data, associated_data, 16
  File "/home/user/.pyenv/versions/somepyenv/lib/python3.6/site-packages/cryptography/hazmat/backends/openssl/aead.py", line 109, in _encrypt
    processed_data = _process_data(backend, ctx, data)
  File "/home/user/.pyenv/versions/somepyenv/lib/python3.6/site-packages/cryptography/hazmat/backends/openssl/aead.py", line 92, in _process_data
    res = backend._lib.EVP_CipherUpdate(ctx, buf, outlen, data, len(data))
OverflowError: integer 4294967297 does not fit '32-bit int'
```

Test environment:
Using Python 3.6.1, cryptography 2.2.2, cffi 1.11.5. Cryptography was installed using pip install onto a venv managed by pyenv.

Suggested course of action:
Update documentation to reflect that the amount of data that can be pushed through `encrypt()` is limited.  This should probably be documented wherever user data could be encrypted and potentially cause a failure for data to cross the cffi interface.


Discussion:
In this particular case we can either document that we only support 2**32 bytes on an `encrypt` call or we could slice the bytes and support larger calls. @vEpiphyte does your use case really need a streaming AEAD rather than a one shot? If you know you're not going to start using any decrypted bytes until after you check the tag then https://cryptography.io/en/latest/hazmat/primitives/symmetric-encryption/?highlight=GCM#cryptography.hazmat.primitives.ciphers.modes.GCM may be what you want.
@reaperhulk My use case doesn't need streaming AEAD, and in fact we're moving away from using the primitives entirely.  I think it would be best to document the limits so that no one else ends up in the boat I found myself in accidentally.

Resolving patch:
diff --git a/src/cryptography/hazmat/primitives/ciphers/aead.py b/src/cryptography/hazmat/primitives/ciphers/aead.py
--- a/src/cryptography/hazmat/primitives/ciphers/aead.py
+++ b/src/cryptography/hazmat/primitives/ciphers/aead.py
@@ -12,6 +12,8 @@
 
 
 class ChaCha20Poly1305(object):
+    _MAX_SIZE = 2 ** 32
+
     def __init__(self, key):
         if not backend.aead_cipher_supported(self):
             raise exceptions.UnsupportedAlgorithm(
@@ -33,6 +35,12 @@ def encrypt(self, nonce, data, associated_data):
         if associated_data is None:
             associated_data = b""
 
+        if len(data) > self._MAX_SIZE or len(associated_data) > self._MAX_SIZE:
+            # This is OverflowError to match what cffi would raise
+            raise OverflowError(
+                "Data or associated data too long. Max 2**32 bytes"
+            )
+
         self._check_params(nonce, data, associated_data)
         return aead._encrypt(
             backend, self, nonce, data, associated_data, 16
@@ -56,6 +64,8 @@ def _check_params(self, nonce, data, associated_data):
 
 
 class AESCCM(object):
+    _MAX_SIZE = 2 ** 32
+
     def __init__(self, key, tag_length=16):
         utils._check_bytes("key", key)
         if len(key) not in (16, 24, 32):
@@ -90,6 +100,12 @@ def encrypt(self, nonce, data, associated_data):
         if associated_data is None:
             associated_data = b""
 
+        if len(data) > self._MAX_SIZE or len(associated_data) > self._MAX_SIZE:
+            # This is OverflowError to match what cffi would raise
+            raise OverflowError(
+                "Data or associated data too long. Max 2**32 bytes"
+            )
+
         self._check_params(nonce, data, associated_data)
         self._validate_lengths(nonce, len(data))
         return aead._encrypt(
@@ -121,6 +137,8 @@ def _check_params(self, nonce, data, associated_data):
 
 
 class AESGCM(object):
+    _MAX_SIZE = 2 ** 32
+
     def __init__(self, key):
         utils._check_bytes("key", key)
         if len(key) not in (16, 24, 32):
@@ -142,6 +160,12 @@ def encrypt(self, nonce, data, associated_data):
         if associated_data is None:
             associated_data = b""
 
+        if len(data) > self._MAX_SIZE or len(associated_data) > self._MAX_SIZE:
+            # This is OverflowError to match what cffi would raise
+            raise OverflowError(
+                "Data or associated data too long. Max 2**32 bytes"
+            )
+
         self._check_params(nonce, data, associated_data)
         return aead._encrypt(
             backend, self, nonce, data, associated_data, 16


Test patch:
diff --git a/tests/hazmat/primitives/test_aead.py b/tests/hazmat/primitives/test_aead.py
--- a/tests/hazmat/primitives/test_aead.py
+++ b/tests/hazmat/primitives/test_aead.py
@@ -22,6 +22,11 @@
 )
 
 
+class FakeData(object):
+    def __len__(self):
+        return 2 ** 32 + 1
+
+
 def _aead_supported(cls):
     try:
         cls(b"0" * 32)
@@ -46,6 +51,17 @@ def test_chacha20poly1305_unsupported_on_older_openssl(backend):
 )
 @pytest.mark.requires_backend_interface(interface=CipherBackend)
 class TestChaCha20Poly1305(object):
+    def test_data_too_large(self):
+        key = ChaCha20Poly1305.generate_key()
+        chacha = ChaCha20Poly1305(key)
+        nonce = b"0" * 12
+
+        with pytest.raises(OverflowError):
+            chacha.encrypt(nonce, FakeData(), b"")
+
+        with pytest.raises(OverflowError):
+            chacha.encrypt(nonce, b"", FakeData())
+
     def test_generate_key(self):
         key = ChaCha20Poly1305.generate_key()
         assert len(key) == 32
@@ -168,6 +184,17 @@ def test_aesccm_unsupported_on_older_openssl(backend):
 )
 @pytest.mark.requires_backend_interface(interface=CipherBackend)
 class TestAESCCM(object):
+    def test_data_too_large(self):
+        key = AESCCM.generate_key(128)
+        aesccm = AESCCM(key)
+        nonce = b"0" * 12
+
+        with pytest.raises(OverflowError):
+            aesccm.encrypt(nonce, FakeData(), b"")
+
+        with pytest.raises(OverflowError):
+            aesccm.encrypt(nonce, b"", FakeData())
+
     def test_default_tag_length(self, backend):
         key = AESCCM.generate_key(128)
         aesccm = AESCCM(key)
@@ -309,6 +336,17 @@ def _load_gcm_vectors():
 
 @pytest.mark.requires_backend_interface(interface=CipherBackend)
 class TestAESGCM(object):
+    def test_data_too_large(self):
+        key = AESGCM.generate_key(128)
+        aesgcm = AESGCM(key)
+        nonce = b"0" * 12
+
+        with pytest.raises(OverflowError):
+            aesgcm.encrypt(nonce, FakeData(), b"")
+
+        with pytest.raises(OverflowError):
+            aesgcm.encrypt(nonce, b"", FakeData())
+
     @pytest.mark.parametrize("vector", _load_gcm_vectors())
     def test_vectors(self, vector):
         key = binascii.unhexlify(vector["key"])