aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2017-07-17 13:10:14 +0200
committerAlex Gaynor <alex.gaynor@gmail.com>2017-07-17 07:10:14 -0400
commita2173583d928cc95977f8dbbb7dd48cc732b24f5 (patch)
tree7a393eb2319936e2402976499213a9eb13c53b20
parent165743ee63d05b343bf7e6b6b0afe0c23be71ca2 (diff)
downloadcryptography-a2173583d928cc95977f8dbbb7dd48cc732b24f5.tar.gz
cryptography-a2173583d928cc95977f8dbbb7dd48cc732b24f5.tar.bz2
cryptography-a2173583d928cc95977f8dbbb7dd48cc732b24f5.zip
add AESGCM AEAD support (#3785)
* add AESGCM AEAD support * remove stray newline * move AESGCM docs above CCM
-rw-r--r--CHANGELOG.rst3
-rw-r--r--docs/hazmat/primitives/aead.rst71
-rw-r--r--docs/hazmat/primitives/symmetric-encryption.rst5
-rw-r--r--src/cryptography/hazmat/backends/openssl/aead.py8
-rw-r--r--src/cryptography/hazmat/primitives/ciphers/aead.py42
-rw-r--r--tests/hazmat/primitives/test_aead.py82
6 files changed, 207 insertions, 4 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 278d977a..aa0e2e90 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -29,6 +29,9 @@ Changelog
: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 6b13edc1..b4e4eaf5 100644
--- a/docs/hazmat/primitives/aead.rst
+++ b/docs/hazmat/primitives/aead.rst
@@ -78,6 +78,75 @@ also support providing integrity for associated data which is not encrypted.
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 @@ also support providing integrity for associated data which is not encrypted.
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 7e05acde..9e27540d 100644
--- a/docs/hazmat/primitives/symmetric-encryption.rst
+++ b/docs/hazmat/primitives/symmetric-encryption.rst
@@ -294,6 +294,11 @@ Modes
.. 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 5402acb3..9cec3e23 100644
--- a/src/cryptography/hazmat/backends/openssl/aead.py
+++ b/src/cryptography/hazmat/backends/openssl/aead.py
@@ -13,13 +13,15 @@ _DECRYPT = 0
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 e2c5e381..07b6bce6 100644
--- a/src/cryptography/hazmat/primitives/ciphers/aead.py
+++ b/src/cryptography/hazmat/primitives/ciphers/aead.py
@@ -118,3 +118,45 @@ class AESCCM(object):
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 27374da9..dc2f357b 100644
--- a/tests/hazmat/primitives/test_aead.py
+++ b/tests/hazmat/primitives/test_aead.py
@@ -12,7 +12,7 @@ import pytest
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 @@ class TestAESCCM(object):
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