Repository: pyca/cryptography

Issue:
Implement One Shot AESGCM
Much like our ChaCha20Poly1305 and AESCCM classes we should do AESGCM. Then we can update our streaming GCM docs to point at the one shot as a better option if you aren't dealing with large volumes of data.


Discussion:

Resolving patch:
diff --git a/src/cryptography/hazmat/backends/openssl/aead.py b/src/cryptography/hazmat/backends/openssl/aead.py
--- a/src/cryptography/hazmat/backends/openssl/aead.py
+++ b/src/cryptography/hazmat/backends/openssl/aead.py
@@ -13,13 +13,15 @@
 
 def _aead_cipher_name(cipher):
     from cryptography.hazmat.primitives.ciphers.aead import (
-        AESCCM, ChaCha20Poly1305
+        AESCCM, AESGCM, ChaCha20Poly1305
     )
     if isinstance(cipher, ChaCha20Poly1305):
         return b"chacha20-poly1305"
-    else:
-        assert isinstance(cipher, AESCCM)
+    elif isinstance(cipher, AESCCM):
         return "aes-{0}-ccm".format(len(cipher._key) * 8).encode("ascii")
+    else:
+        assert isinstance(cipher, AESGCM)
+        return "aes-{0}-gcm".format(len(cipher._key) * 8).encode("ascii")
 
 
 def _aead_setup(backend, cipher_name, key, nonce, tag, tag_len, operation):
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
@@ -118,3 +118,45 @@ def _check_params(self, nonce, data, associated_data):
         utils._check_bytes("associated_data", associated_data)
         if not 7 <= len(nonce) <= 13:
             raise ValueError("Nonce must be between 7 and 13 bytes")
+
+
+class AESGCM(object):
+    def __init__(self, key):
+        utils._check_bytes("key", key)
+        if len(key) not in (16, 24, 32):
+            raise ValueError("AESGCM key must be 128, 192, or 256 bits.")
+
+        self._key = key
+
+    @classmethod
+    def generate_key(cls, bit_length):
+        if not isinstance(bit_length, int):
+            raise TypeError("bit_length must be an integer")
+
+        if bit_length not in (128, 192, 256):
+            raise ValueError("bit_length must be 128, 192, or 256")
+
+        return os.urandom(bit_length // 8)
+
+    def encrypt(self, nonce, data, associated_data):
+        if associated_data is None:
+            associated_data = b""
+
+        self._check_params(nonce, data, associated_data)
+        return aead._encrypt(
+            backend, self, nonce, data, associated_data, 16
+        )
+
+    def decrypt(self, nonce, data, associated_data):
+        if associated_data is None:
+            associated_data = b""
+
+        self._check_params(nonce, data, associated_data)
+        return aead._decrypt(
+            backend, self, nonce, data, associated_data, 16
+        )
+
+    def _check_params(self, nonce, data, associated_data):
+        utils._check_bytes("nonce", nonce)
+        utils._check_bytes("data", data)
+        utils._check_bytes("associated_data", 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
@@ -12,7 +12,7 @@
 from cryptography.exceptions import InvalidTag, UnsupportedAlgorithm, _Reasons
 from cryptography.hazmat.backends.interfaces import CipherBackend
 from cryptography.hazmat.primitives.ciphers.aead import (
-    AESCCM, ChaCha20Poly1305
+    AESCCM, AESGCM, ChaCha20Poly1305
 )
 
 from .utils import _load_all_params
@@ -289,3 +289,83 @@ def test_decrypt_data_too_short(self, backend):
         aesccm = AESCCM(key)
         with pytest.raises(InvalidTag):
             aesccm.decrypt(b"0" * 12, b"0", None)
+
+
+def _load_gcm_vectors():
+    vectors = _load_all_params(
+        os.path.join("ciphers", "AES", "GCM"),
+        [
+            "gcmDecrypt128.rsp",
+            "gcmDecrypt192.rsp",
+            "gcmDecrypt256.rsp",
+            "gcmEncryptExtIV128.rsp",
+            "gcmEncryptExtIV192.rsp",
+            "gcmEncryptExtIV256.rsp",
+        ],
+        load_nist_vectors
+    )
+    return [x for x in vectors if len(x["tag"]) == 32]
+
+
+@pytest.mark.requires_backend_interface(interface=CipherBackend)
+class TestAESGCM(object):
+    @pytest.mark.parametrize("vector", _load_gcm_vectors())
+    def test_vectors(self, vector):
+        key = binascii.unhexlify(vector["key"])
+        nonce = binascii.unhexlify(vector["iv"])
+        aad = binascii.unhexlify(vector["aad"])
+        ct = binascii.unhexlify(vector["ct"])
+        pt = binascii.unhexlify(vector.get("pt", b""))
+        tag = binascii.unhexlify(vector["tag"])
+        aesgcm = AESGCM(key)
+        if vector.get("fail") is True:
+            with pytest.raises(InvalidTag):
+                aesgcm.decrypt(nonce, ct + tag, aad)
+        else:
+            computed_ct = aesgcm.encrypt(nonce, pt, aad)
+            assert computed_ct[:-16] == ct
+            assert computed_ct[-16:] == tag
+            computed_pt = aesgcm.decrypt(nonce, ct + tag, aad)
+            assert computed_pt == pt
+
+    @pytest.mark.parametrize(
+        ("nonce", "data", "associated_data"),
+        [
+            [object(), b"data", b""],
+            [b"0" * 12, object(), b""],
+            [b"0" * 12, b"data", object()]
+        ]
+    )
+    def test_params_not_bytes(self, nonce, data, associated_data, backend):
+        key = AESGCM.generate_key(128)
+        aesgcm = AESGCM(key)
+        with pytest.raises(TypeError):
+            aesgcm.encrypt(nonce, data, associated_data)
+
+        with pytest.raises(TypeError):
+            aesgcm.decrypt(nonce, data, associated_data)
+
+    def test_bad_key(self, backend):
+        with pytest.raises(TypeError):
+            AESGCM(object())
+
+        with pytest.raises(ValueError):
+            AESGCM(b"0" * 31)
+
+    def test_bad_generate_key(self, backend):
+        with pytest.raises(TypeError):
+            AESGCM.generate_key(object())
+
+        with pytest.raises(ValueError):
+            AESGCM.generate_key(129)
+
+    def test_associated_data_none_equal_to_empty_bytestring(self, backend):
+        key = AESGCM.generate_key(128)
+        aesgcm = AESGCM(key)
+        nonce = os.urandom(12)
+        ct1 = aesgcm.encrypt(nonce, b"some_data", None)
+        ct2 = aesgcm.encrypt(nonce, b"some_data", b"")
+        assert ct1 == ct2
+        pt1 = aesgcm.decrypt(nonce, ct1, None)
+        pt2 = aesgcm.decrypt(nonce, ct2, b"")
+        assert pt1 == pt2