add AESGCM AEAD support (#3785)
* add AESGCM AEAD support
* remove stray newline
* move AESGCM docs above CCM
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 278d977..aa0e2e9 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -29,6 +29,9 @@
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305`.
* Added support for
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESCCM`.
+* Added
+ :class:`~cryptography.hazmat.primitives.ciphers.aead.AESGCM`, a "one shot"
+ API for AES GCM encryption.
* Added support for :doc:`/hazmat/primitives/asymmetric/x25519`.
* Added support for serializing and deserializing Diffie-Hellman parameters
with
diff --git a/docs/hazmat/primitives/aead.rst b/docs/hazmat/primitives/aead.rst
index 6b13edc..b4e4eaf 100644
--- a/docs/hazmat/primitives/aead.rst
+++ b/docs/hazmat/primitives/aead.rst
@@ -78,6 +78,75 @@
when the ciphertext has been changed, but will also occur when the
key, nonce, or associated data are wrong.
+.. class:: AESGCM(key)
+
+ .. versionadded:: 2.0
+
+ The AES-GCM construction is composed of the
+ :class:`~cryptography.hazmat.primitives.ciphers.algorithms.AES` block
+ cipher utilizing Galois Counter Mode (GCM).
+
+ :param bytes key: A 128, 192, or 256-bit key. This **must** be kept secret.
+
+ .. doctest::
+
+ >>> import os
+ >>> from cryptography.hazmat.primitives.ciphers.aead import AESGCM
+ >>> data = b"a secret message"
+ >>> aad = b"authenticated but unencrypted data"
+ >>> key = AESGCM.generate_key(bit_length=128)
+ >>> aesgcm = AESGCM(key)
+ >>> nonce = os.urandom(12)
+ >>> ct = aesgcm.encrypt(nonce, data, aad)
+ >>> aesgcm.decrypt(nonce, ct, aad)
+ 'a secret message'
+
+ .. classmethod:: generate_key(bit_length)
+
+ Securely generates a random AES-GCM key.
+
+ :param bit_length: The bit length of the key to generate. Must be
+ 128, 192, or 256.
+
+ :returns bytes: The generated key.
+
+ .. method:: encrypt(nonce, data, associated_data)
+
+ .. warning::
+
+ Reuse of a ``nonce`` with a given ``key`` compromises the security
+ of any message with that ``nonce`` and ``key`` pair.
+
+ Encrypts and authenticates the ``data`` provided as well as
+ authenticating the ``associated_data``. The output of this can be
+ passed directly to the ``decrypt`` method.
+
+ :param bytes nonce: NIST `recommends a 96-bit IV length`_ for best
+ performance but it can be up to 2\ :sup:`64` - 1 bits.
+ **NEVER REUSE A NONCE** with a key.
+ :param bytes data: The data to encrypt.
+ :param bytes associated_data: Additional data that should be
+ authenticated with the key, but is not encrypted. Can be ``None``.
+ :returns bytes: The ciphertext bytes with the 16 byte tag appended.
+
+ .. method:: decrypt(nonce, data, associated_data)
+
+ Decrypts the ``data`` and authenticates the ``associated_data``. If you
+ called encrypt with ``associated_data`` you must pass the same
+ ``associated_data`` in decrypt or the integrity check will fail.
+
+ :param bytes nonce: NIST `recommends a 96-bit IV length`_ for best
+ performance but it can be up to 2\ :sup:`64` - 1 bits.
+ **NEVER REUSE A NONCE** with a key.
+ :param bytes data: The data to decrypt (with tag appended).
+ :param bytes associated_data: Additional data to authenticate. Can be
+ ``None`` if none was passed during encryption.
+ :returns bytes: The original plaintext.
+ :raises cryptography.exceptions.InvalidTag: If the authentication tag
+ doesn't validate this exception will be raised. This will occur
+ when the ciphertext has been changed, but will also occur when the
+ key, nonce, or associated data are wrong.
+
.. class:: AESCCM(key, tag_length=16)
.. versionadded:: 2.0
@@ -161,3 +230,5 @@
doesn't validate this exception will be raised. This will occur
when the ciphertext has been changed, but will also occur when the
key, nonce, or associated data are wrong.
+
+.. _`recommends a 96-bit IV length`: http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf
diff --git a/docs/hazmat/primitives/symmetric-encryption.rst b/docs/hazmat/primitives/symmetric-encryption.rst
index 7e05acd..9e27540 100644
--- a/docs/hazmat/primitives/symmetric-encryption.rst
+++ b/docs/hazmat/primitives/symmetric-encryption.rst
@@ -294,6 +294,11 @@
.. danger::
+ If you are encrypting data that can fit into memory you should strongly
+ consider using
+ :class:`~cryptography.hazmat.primitives.ciphers.aead.AESGCM` instead
+ of this.
+
When using this mode you **must** not use the decrypted data until
the appropriate finalization method
(:meth:`~cryptography.hazmat.primitives.ciphers.CipherContext.finalize`
diff --git a/src/cryptography/hazmat/backends/openssl/aead.py b/src/cryptography/hazmat/backends/openssl/aead.py
index 5402acb..9cec3e2 100644
--- 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
index e2c5e38..07b6bce 100644
--- a/src/cryptography/hazmat/primitives/ciphers/aead.py
+++ b/src/cryptography/hazmat/primitives/ciphers/aead.py
@@ -118,3 +118,45 @@
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)
diff --git a/tests/hazmat/primitives/test_aead.py b/tests/hazmat/primitives/test_aead.py
index 27374da..dc2f357 100644
--- 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 @@
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