Skip to content

Commit 911e73b

Browse files
authored
implement decrypt_into for aesccm (#13785)
1 parent c1a4af6 commit 911e73b

File tree

5 files changed

+148
-23
lines changed

5 files changed

+148
-23
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ 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.AESCCM`,
5152
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESGCM`,
5253
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV`, and
5354
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to

docs/hazmat/primitives/aead.rst

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -767,4 +767,33 @@ also support providing integrity for associated data which is not encrypted.
767767
when the ciphertext has been changed, but will also occur when the
768768
key, nonce, or associated data are wrong.
769769

770+
.. method:: decrypt_into(nonce, data, associated_data, buf)
771+
772+
.. versionadded:: 47.0.0
773+
774+
Decrypts the ``data`` and authenticates the ``associated_data``. If you
775+
called encrypt with ``associated_data`` you must pass the same
776+
``associated_data`` in decrypt or the integrity check will fail. The
777+
output is written into the ``buf`` parameter.
778+
779+
:param nonce: A value of between 7 and 13 bytes. This
780+
is the same value used when you originally called encrypt.
781+
**NEVER REUSE A NONCE** with a key.
782+
:type nonce: :term:`bytes-like`
783+
:param data: The data to decrypt (with tag appended).
784+
:type data: :term:`bytes-like`
785+
:param associated_data: Additional data to authenticate. Can be
786+
``None`` if none was passed during encryption.
787+
:type associated_data: :term:`bytes-like`
788+
:param buf: A writable :term:`bytes-like` object that must be exactly
789+
``len(data) - tag_length`` bytes. The plaintext will be written to
790+
this buffer.
791+
:returns int: The number of bytes written to the buffer (always
792+
``len(data) - tag_length``).
793+
:raises ValueError: If the buffer is not the correct size.
794+
:raises cryptography.exceptions.InvalidTag: If the authentication tag
795+
doesn't validate this exception will be raised. This will occur
796+
when the ciphertext has been changed, but will also occur when the
797+
key, nonce, or associated data are wrong.
798+
770799
.. _`recommends a 96-bit IV length`: https://csrc.nist.gov/pubs/sp/800/38/d/final

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,13 @@ class AESCCM:
9191
data: Buffer,
9292
associated_data: Buffer | None,
9393
) -> bytes: ...
94+
def decrypt_into(
95+
self,
96+
nonce: Buffer,
97+
data: Buffer,
98+
associated_data: Buffer | None,
99+
buf: Buffer,
100+
) -> int: ...
94101

95102
class AESSIV:
96103
def __init__(self, key: Buffer) -> None: ...

src/rust/src/backend/aead.rs

Lines changed: 52 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -351,26 +351,6 @@ impl LazyEvpCipherAead {
351351
)
352352
}
353353

354-
fn decrypt<'p>(
355-
&self,
356-
py: pyo3::Python<'p>,
357-
ciphertext: &[u8],
358-
aad: Option<Aad<'_>>,
359-
nonce: Option<&[u8]>,
360-
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
361-
if ciphertext.len() < self.tag_len {
362-
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
363-
}
364-
Ok(pyo3::types::PyBytes::new_with(
365-
py,
366-
ciphertext.len() - self.tag_len,
367-
|b| {
368-
self.decrypt_into(py, ciphertext, aad, nonce, b)?;
369-
Ok(())
370-
},
371-
)?)
372-
}
373-
374354
fn decrypt_into(
375355
&self,
376356
py: pyo3::Python<'_>,
@@ -1064,27 +1044,76 @@ impl AesCcm {
10641044
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
10651045
let nonce_bytes = nonce.as_bytes();
10661046
let data_bytes = data.as_bytes();
1047+
1048+
if nonce_bytes.len() < 7 || nonce_bytes.len() > 13 {
1049+
return Err(CryptographyError::from(
1050+
pyo3::exceptions::PyValueError::new_err("Nonce must be between 7 and 13 bytes"),
1051+
));
1052+
}
1053+
if data_bytes.len() < self.tag_length {
1054+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
1055+
}
1056+
1057+
Ok(pyo3::types::PyBytes::new_with(
1058+
py,
1059+
data_bytes.len() - self.tag_length,
1060+
|b| {
1061+
let buf = CffiMutBuf::from_bytes(py, b);
1062+
self.decrypt_into(py, nonce, data, associated_data, buf)?;
1063+
Ok(())
1064+
},
1065+
)?)
1066+
}
1067+
1068+
#[pyo3(signature = (nonce, data, associated_data, buf))]
1069+
fn decrypt_into(
1070+
&self,
1071+
py: pyo3::Python<'_>,
1072+
nonce: CffiBuf<'_>,
1073+
data: CffiBuf<'_>,
1074+
associated_data: Option<CffiBuf<'_>>,
1075+
mut buf: CffiMutBuf<'_>,
1076+
) -> CryptographyResult<usize> {
1077+
let nonce_bytes = nonce.as_bytes();
1078+
let data_bytes = data.as_bytes();
10671079
let aad = associated_data.map(Aad::Single);
10681080

10691081
if nonce_bytes.len() < 7 || nonce_bytes.len() > 13 {
10701082
return Err(CryptographyError::from(
10711083
pyo3::exceptions::PyValueError::new_err("Nonce must be between 7 and 13 bytes"),
10721084
));
10731085
}
1086+
1087+
if data_bytes.len() < self.tag_length {
1088+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
1089+
}
1090+
10741091
// For information about computing this, see
10751092
// https://tools.ietf.org/html/rfc3610#section-2.1
10761093
let l_val = 15 - nonce_bytes.len();
10771094
let max_length = 1usize.checked_shl(8 * l_val as u32);
10781095
// If `max_length` overflowed, then it's not possible for data to be
10791096
// longer than it.
1080-
let pt_length = data_bytes.len().saturating_sub(self.tag_length);
1081-
if max_length.map(|v| v < pt_length).unwrap_or(false) {
1097+
let expected_len = data_bytes.len() - self.tag_length;
1098+
if max_length.map(|v| v < expected_len).unwrap_or(false) {
10821099
return Err(CryptographyError::from(
10831100
pyo3::exceptions::PyValueError::new_err("Data too long for nonce"),
10841101
));
10851102
}
10861103

1087-
self.ctx.decrypt(py, data_bytes, aad, Some(nonce_bytes))
1104+
if buf.as_mut_bytes().len() != expected_len {
1105+
return Err(CryptographyError::from(
1106+
pyo3::exceptions::PyValueError::new_err(format!(
1107+
"buffer must be {} bytes",
1108+
expected_len
1109+
)),
1110+
));
1111+
}
1112+
1113+
self.ctx
1114+
.decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
1115+
1116+
Ok(expected_len)
10881117
}
10891118
}
10901119

tests/hazmat/primitives/test_aead.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,14 @@ def test_invalid_nonce_length(self, backend):
321321
with pytest.raises(ValueError):
322322
aesccm.encrypt(nonce[:6], pt, None)
323323

324+
with pytest.raises(ValueError):
325+
buf = bytearray(16)
326+
aesccm.decrypt_into(nonce, b"x" * 20, None, buf)
327+
328+
with pytest.raises(ValueError):
329+
buf = bytearray(16)
330+
aesccm.decrypt_into(nonce[:6], b"x" * 20, None, buf)
331+
324332
def test_vectors(self, subtests, backend):
325333
vectors = _load_all_params(
326334
os.path.join("ciphers", "AES", "CCM"),
@@ -423,6 +431,10 @@ def test_decrypt_data_too_short(self, backend):
423431
with pytest.raises(InvalidTag):
424432
aesccm.decrypt(b"0" * 12, b"0", None)
425433

434+
with pytest.raises(InvalidTag):
435+
buf = bytearray(16)
436+
aesccm.decrypt_into(b"0" * 12, b"0", None, buf)
437+
426438
def test_buffer_protocol(self, backend):
427439
key = AESCCM.generate_key(128)
428440
aesccm = AESCCM(key)
@@ -472,6 +484,53 @@ def test_encrypt_into_buffer_incorrect_size(self, ptlen, buflen, backend):
472484
with pytest.raises(ValueError, match="buffer must be"):
473485
aesccm.encrypt_into(nonce, pt, None, buf)
474486

487+
def test_decrypt_into(self, backend):
488+
key = AESCCM.generate_key(128)
489+
aesccm = AESCCM(key)
490+
nonce = os.urandom(12)
491+
pt = b"decrypt me"
492+
ad = b"additional"
493+
ct = aesccm.encrypt(nonce, pt, ad)
494+
buf = bytearray(len(pt))
495+
n = aesccm.decrypt_into(nonce, ct, ad, buf)
496+
assert n == len(pt)
497+
assert buf == pt
498+
499+
@pytest.mark.parametrize(
500+
("ctlen", "buflen"), [(26, 9), (26, 11), (31, 14), (36, 21)]
501+
)
502+
def test_decrypt_into_buffer_incorrect_size(self, ctlen, buflen, backend):
503+
key = AESCCM.generate_key(128)
504+
aesccm = AESCCM(key)
505+
nonce = os.urandom(12)
506+
ct = b"x" * ctlen
507+
buf = bytearray(buflen)
508+
with pytest.raises(ValueError, match="buffer must be"):
509+
aesccm.decrypt_into(nonce, ct, None, buf)
510+
511+
def test_decrypt_into_invalid_tag(self, backend):
512+
key = AESCCM.generate_key(128)
513+
aesccm = AESCCM(key)
514+
nonce = os.urandom(12)
515+
pt = b"some data"
516+
ad = b"additional"
517+
ct = aesccm.encrypt(nonce, pt, ad)
518+
# Corrupt the ciphertext
519+
corrupted_ct = bytearray(ct)
520+
corrupted_ct[0] ^= 1
521+
buf = bytearray(len(pt))
522+
with pytest.raises(InvalidTag):
523+
aesccm.decrypt_into(nonce, bytes(corrupted_ct), ad, buf)
524+
525+
def test_decrypt_into_nonce_too_long(self, backend):
526+
key = AESCCM.generate_key(128)
527+
aesccm = AESCCM(key)
528+
pt = b"encrypt me" * 6600
529+
nonce = os.urandom(13)
530+
buf = bytearray(len(pt))
531+
with pytest.raises(ValueError, match="Data too long for nonce"):
532+
aesccm.decrypt_into(nonce, pt, None, buf)
533+
475534

476535
def _load_gcm_vectors():
477536
vectors = _load_all_params(

0 commit comments

Comments
 (0)