Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ Changelog
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to
allow encrypting directly into a pre-allocated buffer.
* Added ``decrypt_into`` methods to
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV` and
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESGCM`,
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV`, and
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to
allow decrypting directly into a pre-allocated buffer.

Expand Down
29 changes: 29 additions & 0 deletions docs/hazmat/primitives/aead.rst
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,35 @@ 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.

.. method:: decrypt_into(nonce, data, associated_data, buf)

.. versionadded:: 47.0.0

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. The
output is written into the ``buf`` parameter.

:param nonce: NIST `recommends a 96-bit IV length`_ for best
performance but it can be up to 2\ :sup:`64` - 1 :term:`bits`.
**NEVER REUSE A NONCE** with a key.
:type nonce: :term:`bytes-like`
:param data: The data to decrypt (with tag appended).
:type data: :term:`bytes-like`
:param associated_data: Additional data to authenticate. Can be
``None`` if none was passed during encryption.
:type associated_data: :term:`bytes-like`
:param buf: A writable :term:`bytes-like` object that must be exactly
``len(data) - 16`` bytes. The plaintext will be written to this
buffer.
:returns int: The number of bytes written to the buffer (always
``len(data) - 16``).
:raises ValueError: If the buffer is not the correct size.
: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:: AESGCMSIV(key)

.. versionadded:: 42.0.0
Expand Down
7 changes: 7 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/aead.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ class AESGCM:
associated_data: Buffer | None,
buf: Buffer,
) -> int: ...
def decrypt_into(
self,
nonce: Buffer,
data: Buffer,
associated_data: Buffer | None,
buf: Buffer,
) -> int: ...

class ChaCha20Poly1305:
def __init__(self, key: Buffer) -> None: ...
Expand Down
52 changes: 51 additions & 1 deletion src/rust/src/backend/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,40 @@ impl AesGcm {
associated_data: Option<CffiBuf<'_>>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
let nonce_bytes = nonce.as_bytes();
let data_bytes = data.as_bytes();

if nonce_bytes.len() < 8 || nonce_bytes.len() > 128 {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err("Nonce must be between 8 and 128 bytes"),
));
}

if data_bytes.len() < self.ctx.tag_len {
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
}

Ok(pyo3::types::PyBytes::new_with(
py,
data_bytes.len() - self.ctx.tag_len,
|b| {
let buf = CffiMutBuf::from_bytes(py, b);
self.decrypt_into(py, nonce, data, associated_data, buf)?;
Ok(())
},
)?)
}

#[pyo3(signature = (nonce, data, associated_data, buf))]
fn decrypt_into(
&self,
py: pyo3::Python<'_>,
nonce: CffiBuf<'_>,
data: CffiBuf<'_>,
associated_data: Option<CffiBuf<'_>>,
mut buf: CffiMutBuf<'_>,
) -> CryptographyResult<usize> {
let nonce_bytes = nonce.as_bytes();
let data_bytes = data.as_bytes();
let aad = associated_data.map(Aad::Single);

if nonce_bytes.len() < 8 || nonce_bytes.len() > 128 {
Expand All @@ -855,8 +889,24 @@ impl AesGcm {
));
}

if data_bytes.len() < self.ctx.tag_len {
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
}

let expected_len = data_bytes.len() - self.ctx.tag_len;
if buf.as_mut_bytes().len() != expected_len {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(format!(
"buffer must be {} bytes",
expected_len
)),
));
}

self.ctx
.decrypt(py, data.as_bytes(), aad, Some(nonce_bytes))
.decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;

Ok(expected_len)
}
}

Expand Down
45 changes: 45 additions & 0 deletions tests/hazmat/primitives/test_aead.py
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,10 @@ def test_decrypt_data_too_short(self):
with pytest.raises(InvalidTag):
aesgcm.decrypt(b"0" * 12, b"0", None)

with pytest.raises(InvalidTag):
buf = bytearray(16)
aesgcm.decrypt_into(b"0" * 12, b"0", None, buf)

def test_vectors(self, backend, subtests):
vectors = _load_gcm_vectors()
for vector in vectors:
Expand Down Expand Up @@ -573,6 +577,9 @@ def test_invalid_nonce_length(self, length, backend):
aesgcm.encrypt_into(b"\x00" * length, b"hi", None, buf)
with pytest.raises(ValueError):
aesgcm.decrypt(b"\x00" * length, b"hi", None)
with pytest.raises(ValueError):
buf = bytearray(16)
aesgcm.decrypt_into(b"\x00" * length, b"hi", None, buf)

def test_bad_key(self, backend):
with pytest.raises(TypeError):
Expand Down Expand Up @@ -650,6 +657,44 @@ def test_encrypt_into_buffer_incorrect_size(self, ptlen, buflen, backend):
with pytest.raises(ValueError, match="buffer must be"):
aesgcm.encrypt_into(nonce, pt, None, buf)

def test_decrypt_into(self, backend):
key = AESGCM.generate_key(128)
aesgcm = AESGCM(key)
nonce = os.urandom(12)
pt = b"decrypt me"
ad = b"additional"
ct = aesgcm.encrypt(nonce, pt, ad)
buf = bytearray(len(pt))
n = aesgcm.decrypt_into(nonce, ct, ad, buf)
assert n == len(pt)
assert buf == pt

@pytest.mark.parametrize(
("ctlen", "buflen"), [(26, 9), (26, 11), (31, 14), (36, 21)]
)
def test_decrypt_into_buffer_incorrect_size(self, ctlen, buflen, backend):
key = AESGCM.generate_key(128)
aesgcm = AESGCM(key)
nonce = os.urandom(12)
ct = b"x" * ctlen
buf = bytearray(buflen)
with pytest.raises(ValueError, match="buffer must be"):
aesgcm.decrypt_into(nonce, ct, None, buf)

def test_decrypt_into_invalid_tag(self, backend):
key = AESGCM.generate_key(128)
aesgcm = AESGCM(key)
nonce = os.urandom(12)
pt = b"some data"
ad = b"additional"
ct = aesgcm.encrypt(nonce, pt, ad)
# Corrupt the ciphertext
corrupted_ct = bytearray(ct)
corrupted_ct[0] ^= 1
buf = bytearray(len(pt))
with pytest.raises(InvalidTag):
aesgcm.decrypt_into(nonce, bytes(corrupted_ct), ad, buf)


@pytest.mark.skipif(
_aead_supported(AESOCB3),
Expand Down
Loading