Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add AESGCM AEAD support #3785

Merged
merged 3 commits into from
Jul 17, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
71 changes: 71 additions & 0 deletions docs/hazmat/primitives/aead.rst
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,74 @@ 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.

.. class:: AESGCM(key)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document this above CCM


.. 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.

.. _`recommends a 96-bit IV length`: http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf
5 changes: 5 additions & 0 deletions docs/hazmat/primitives/symmetric-encryption.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
8 changes: 5 additions & 3 deletions src/cryptography/hazmat/backends/openssl/aead.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
42 changes: 42 additions & 0 deletions src/cryptography/hazmat/primitives/ciphers/aead.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
82 changes: 81 additions & 1 deletion tests/hazmat/primitives/test_aead.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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