Skip to content

Commit 4d22f7d

Browse files
committed
implement decrypt_into for aesgcm
1 parent 2fe1c41 commit 4d22f7d

File tree

5 files changed

+134
-2
lines changed

5 files changed

+134
-2
lines changed

CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ Changelog
4848
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to
4949
allow encrypting directly into a pre-allocated buffer.
5050
* Added ``decrypt_into`` methods to
51-
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV` and
51+
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESGCM`,
52+
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV`, and
5253
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to
5354
allow decrypting directly into a pre-allocated buffer.
5455

docs/hazmat/primitives/aead.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,35 @@ also support providing integrity for associated data which is not encrypted.
260260
when the ciphertext has been changed, but will also occur when the
261261
key, nonce, or associated data are wrong.
262262

263+
.. method:: decrypt_into(nonce, data, associated_data, buf)
264+
265+
.. versionadded:: 47.0.0
266+
267+
Decrypts the ``data`` and authenticates the ``associated_data``. If you
268+
called encrypt with ``associated_data`` you must pass the same
269+
``associated_data`` in decrypt or the integrity check will fail. The
270+
output is written into the ``buf`` parameter.
271+
272+
:param nonce: NIST `recommends a 96-bit IV length`_ for best
273+
performance but it can be up to 2\ :sup:`64` - 1 :term:`bits`.
274+
**NEVER REUSE A NONCE** with a key.
275+
:type nonce: :term:`bytes-like`
276+
:param data: The data to decrypt (with tag appended).
277+
:type data: :term:`bytes-like`
278+
:param associated_data: Additional data to authenticate. Can be
279+
``None`` if none was passed during encryption.
280+
:type associated_data: :term:`bytes-like`
281+
:param buf: A writable :term:`bytes-like` object that must be exactly
282+
``len(data) - 16`` bytes. The plaintext will be written to this
283+
buffer.
284+
:returns int: The number of bytes written to the buffer (always
285+
``len(data) - 16``).
286+
:raises ValueError: If the buffer is not the correct size.
287+
:raises cryptography.exceptions.InvalidTag: If the authentication tag
288+
doesn't validate this exception will be raised. This will occur
289+
when the ciphertext has been changed, but will also occur when the
290+
key, nonce, or associated data are wrong.
291+
263292
.. class:: AESGCMSIV(key)
264293

265294
.. versionadded:: 42.0.0

src/cryptography/hazmat/bindings/_rust/openssl/aead.pyi

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ class AESGCM:
2929
associated_data: Buffer | None,
3030
buf: Buffer,
3131
) -> int: ...
32+
def decrypt_into(
33+
self,
34+
nonce: Buffer,
35+
data: Buffer,
36+
associated_data: Buffer | None,
37+
buf: Buffer,
38+
) -> int: ...
3239

3340
class ChaCha20Poly1305:
3441
def __init__(self, key: Buffer) -> None: ...

src/rust/src/backend/aead.rs

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,40 @@ impl AesGcm {
847847
associated_data: Option<CffiBuf<'_>>,
848848
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
849849
let nonce_bytes = nonce.as_bytes();
850+
let data_bytes = data.as_bytes();
851+
852+
if nonce_bytes.len() < 8 || nonce_bytes.len() > 128 {
853+
return Err(CryptographyError::from(
854+
pyo3::exceptions::PyValueError::new_err("Nonce must be between 8 and 128 bytes"),
855+
));
856+
}
857+
858+
if data_bytes.len() < self.ctx.tag_len {
859+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
860+
}
861+
862+
Ok(pyo3::types::PyBytes::new_with(
863+
py,
864+
data_bytes.len() - self.ctx.tag_len,
865+
|b| {
866+
let buf = CffiMutBuf::from_bytes(py, b);
867+
self.decrypt_into(py, nonce, data, associated_data, buf)?;
868+
Ok(())
869+
},
870+
)?)
871+
}
872+
873+
#[pyo3(signature = (nonce, data, associated_data, buf))]
874+
fn decrypt_into(
875+
&self,
876+
py: pyo3::Python<'_>,
877+
nonce: CffiBuf<'_>,
878+
data: CffiBuf<'_>,
879+
associated_data: Option<CffiBuf<'_>>,
880+
mut buf: CffiMutBuf<'_>,
881+
) -> CryptographyResult<usize> {
882+
let nonce_bytes = nonce.as_bytes();
883+
let data_bytes = data.as_bytes();
850884
let aad = associated_data.map(Aad::Single);
851885

852886
if nonce_bytes.len() < 8 || nonce_bytes.len() > 128 {
@@ -855,8 +889,24 @@ impl AesGcm {
855889
));
856890
}
857891

892+
if data_bytes.len() < self.ctx.tag_len {
893+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
894+
}
895+
896+
let expected_len = data_bytes.len() - self.ctx.tag_len;
897+
if buf.as_mut_bytes().len() != expected_len {
898+
return Err(CryptographyError::from(
899+
pyo3::exceptions::PyValueError::new_err(format!(
900+
"buffer must be {} bytes",
901+
expected_len
902+
)),
903+
));
904+
}
905+
858906
self.ctx
859-
.decrypt(py, data.as_bytes(), aad, Some(nonce_bytes))
907+
.decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
908+
909+
Ok(expected_len)
860910
}
861911
}
862912

tests/hazmat/primitives/test_aead.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,10 @@ def test_decrypt_data_too_short(self):
513513
with pytest.raises(InvalidTag):
514514
aesgcm.decrypt(b"0" * 12, b"0", None)
515515

516+
with pytest.raises(InvalidTag):
517+
buf = bytearray(16)
518+
aesgcm.decrypt_into(b"0" * 12, b"0", None, buf)
519+
516520
def test_vectors(self, backend, subtests):
517521
vectors = _load_gcm_vectors()
518522
for vector in vectors:
@@ -573,6 +577,9 @@ def test_invalid_nonce_length(self, length, backend):
573577
aesgcm.encrypt_into(b"\x00" * length, b"hi", None, buf)
574578
with pytest.raises(ValueError):
575579
aesgcm.decrypt(b"\x00" * length, b"hi", None)
580+
with pytest.raises(ValueError):
581+
buf = bytearray(16)
582+
aesgcm.decrypt_into(b"\x00" * length, b"hi", None, buf)
576583

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

660+
def test_decrypt_into(self, backend):
661+
key = AESGCM.generate_key(128)
662+
aesgcm = AESGCM(key)
663+
nonce = os.urandom(12)
664+
pt = b"decrypt me"
665+
ad = b"additional"
666+
ct = aesgcm.encrypt(nonce, pt, ad)
667+
buf = bytearray(len(pt))
668+
n = aesgcm.decrypt_into(nonce, ct, ad, buf)
669+
assert n == len(pt)
670+
assert buf == pt
671+
672+
@pytest.mark.parametrize(
673+
("ctlen", "buflen"), [(26, 9), (26, 11), (31, 14), (36, 21)]
674+
)
675+
def test_decrypt_into_buffer_incorrect_size(self, ctlen, buflen, backend):
676+
key = AESGCM.generate_key(128)
677+
aesgcm = AESGCM(key)
678+
nonce = os.urandom(12)
679+
ct = b"x" * ctlen
680+
buf = bytearray(buflen)
681+
with pytest.raises(ValueError, match="buffer must be"):
682+
aesgcm.decrypt_into(nonce, ct, None, buf)
683+
684+
def test_decrypt_into_invalid_tag(self, backend):
685+
key = AESGCM.generate_key(128)
686+
aesgcm = AESGCM(key)
687+
nonce = os.urandom(12)
688+
pt = b"some data"
689+
ad = b"additional"
690+
ct = aesgcm.encrypt(nonce, pt, ad)
691+
# Corrupt the ciphertext
692+
corrupted_ct = bytearray(ct)
693+
corrupted_ct[0] ^= 1
694+
buf = bytearray(len(pt))
695+
with pytest.raises(InvalidTag):
696+
aesgcm.decrypt_into(nonce, bytes(corrupted_ct), ad, buf)
697+
653698

654699
@pytest.mark.skipif(
655700
_aead_supported(AESOCB3),

0 commit comments

Comments
 (0)