Repository: pyca/cryptography

Issue:
AESSIV Encryption/Decryption fails if empty data is passed
## Issue description

If an empty byte string is passed to `data` parameter of methods `encrypt` and `decrypt` of `AESSIV`, operation fails with `InternalError`.

## Steps to reproduce bug

```python
from cryptography.hazmat.primitives.ciphers import aead

key = bytes(32)
data = b""

cipher = aead.AESSIV(key)
output = cipher.encrypt(data, None)  # raises `InternalError`
```

## cryptography installation

cryptography is installed via poetry with version constraint >=35.0.0:

```toml
[tool.poetry.dependencies]
python = "^3.8"
cryptography = ">=35.0.0"
```

## Required Version numbers

- `pip` - 22.2.2
- `cffi` - 1.15.1
- `cryptography` - 38.0.1
- `setuptools` - 65.3.0



Discussion:
Thanks for the report, we should definitely not `InternalError` here. I don't really think an empty encryption under SIV has any meaning so we probably should just disallow zero bytes as a `ValueError`.
Wouldn't it be valid to just return an empty byte string as output? It is consistent with other modes like AESCCM.
OpenSSL itself is erroring when trying to do this with zero bytes, so either we're calling it wrong (possible, but unlikely) or it does not currently support allowing zero byte pt. It wouldn't be an empty output since it should have a 16 byte output that is effectively the MAC (the SIV).
> Wouldn't it be valid to just return an empty byte string as output? It is consistent with other modes like AESCCM.

You're right, @reaperhulk. The cipher is supposed to return the MAC tag. In fact, `Cryptodome` does that too, instead of erroring out:

```python
from Cryptodome.Cipher import AES

key = bytes(32)
cipher = AES.new(key, AES.MODE_SIV)
output, mac_tag = cipher.encrypt_and_digest(data)

print(output2, mac_tag)
```

I'm not sure about the handling of OpenSSL error. It is preferable to return just the MAC tag, if OpenSSL allows that.
OpenSSL does not currently support this at all. This is inconsistent with their other AEADs, so we could try raising an issue with them, but it's not evident that they think there's a useful reason for this to work. Regardless, for now we'll need to make this a ValueError on cryptography even if they do decide to ultimately fix it.

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
@@ -366,6 +366,8 @@ def _check_params(
         associated_data: typing.List,
     ) -> None:
         utils._check_bytes("data", data)
+        if len(data) == 0:
+            raise ValueError("data must not be zero length")
         if not isinstance(associated_data, list) or not all(
             isinstance(x, bytes) for x in associated_data
         ):


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
@@ -625,7 +625,17 @@ def test_data_too_large(self):
             aessiv.encrypt(FakeData(), None)
 
         with pytest.raises(OverflowError):
-            aessiv.encrypt(b"", [FakeData()])
+            aessiv.encrypt(b"irrelevant", [FakeData()])
+
+    def test_no_empty_encryption(self):
+        key = AESSIV.generate_key(256)
+        aessiv = AESSIV(key)
+
+        with pytest.raises(ValueError):
+            aessiv.encrypt(b"", None)
+
+        with pytest.raises(ValueError):
+            aessiv.decrypt(b"", None)
 
     def test_vectors(self, backend, subtests):
         vectors = load_vectors_from_file(