Skip to content

Commit 2fe1c41

Browse files
authored
implement decrypt_into for chacha20poly1305 (#13783)
* implement decrypt_into for chacha20poly1305 * fix tests maybe * simplify
1 parent 4b0205a commit 2fe1c41

File tree

5 files changed

+185
-31
lines changed

5 files changed

+185
-31
lines changed

CHANGELOG.rst

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,10 @@ Changelog
4747
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV`, and
4848
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to
4949
allow encrypting directly into a pre-allocated buffer.
50-
* Added a ``decrypt_into`` method to
51-
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV` to allow
52-
decrypting directly into a pre-allocated buffer.
50+
* Added ``decrypt_into`` methods to
51+
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV` and
52+
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to
53+
allow decrypting directly into a pre-allocated buffer.
5354

5455
.. _v46-0-3:
5556

docs/hazmat/primitives/aead.rst

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,34 @@ also support providing integrity for associated data which is not encrypted.
116116
when the ciphertext has been changed, but will also occur when the
117117
key, nonce, or associated data are wrong.
118118

119+
.. method:: decrypt_into(nonce, data, associated_data, buf)
120+
121+
.. versionadded:: 47.0.0
122+
123+
Decrypts the ``data`` and authenticates the ``associated_data``. If you
124+
called encrypt with ``associated_data`` you must pass the same
125+
``associated_data`` in decrypt or the integrity check will fail. The
126+
output is written into the ``buf`` parameter.
127+
128+
:param nonce: A 12 byte value. **NEVER REUSE A NONCE** with a
129+
key.
130+
:type nonce: :term:`bytes-like`
131+
:param data: The data to decrypt (with tag appended).
132+
:type data: :term:`bytes-like`
133+
:param associated_data: Additional data to authenticate. Can be
134+
``None`` if none was passed during encryption.
135+
:type associated_data: :term:`bytes-like`
136+
:param buf: A writable :term:`bytes-like` object that must be exactly
137+
``len(data) - 16`` bytes. The plaintext will be written to this
138+
buffer.
139+
:returns int: The number of bytes written to the buffer (always
140+
``len(data) - 16``).
141+
:raises ValueError: If the buffer is not the correct size.
142+
:raises cryptography.exceptions.InvalidTag: If the authentication tag
143+
doesn't validate this exception will be raised. This will occur
144+
when the ciphertext has been changed, but will also occur when the
145+
key, nonce, or associated data are wrong.
146+
119147
.. class:: AESGCM(key)
120148

121149
.. versionadded:: 2.0

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ class ChaCha20Poly1305:
5353
data: Buffer,
5454
associated_data: Buffer | None,
5555
) -> bytes: ...
56+
def decrypt_into(
57+
self,
58+
nonce: Buffer,
59+
data: Buffer,
60+
associated_data: Buffer | None,
61+
buf: Buffer,
62+
) -> int: ...
5663

5764
class AESCCM:
5865
def __init__(self, key: Buffer, tag_length: int = 16) -> None: ...

src/rust/src/backend/aead.rs

Lines changed: 100 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -358,46 +358,50 @@ impl LazyEvpCipherAead {
358358
aad: Option<Aad<'_>>,
359359
nonce: Option<&[u8]>,
360360
) -> 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+
374+
fn decrypt_into(
375+
&self,
376+
py: pyo3::Python<'_>,
377+
ciphertext: &[u8],
378+
aad: Option<Aad<'_>>,
379+
nonce: Option<&[u8]>,
380+
buf: &mut [u8],
381+
) -> CryptographyResult<()> {
361382
let key_buf = self.key.bind(py).extract::<CffiBuf<'_>>()?;
362383

363384
let mut decryption_ctx = openssl::cipher_ctx::CipherCtx::new()?;
364385
if self.is_ccm {
365386
decryption_ctx.decrypt_init(Some(self.cipher), None, None)?;
366387
decryption_ctx.set_iv_length(nonce.as_ref().unwrap().len())?;
367-
368-
if ciphertext.len() < self.tag_len {
369-
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
370-
}
371-
372388
let (_, tag) = ciphertext.split_at(ciphertext.len() - self.tag_len);
373389
decryption_ctx.set_tag(tag)?;
374-
375390
decryption_ctx.decrypt_init(None, Some(key_buf.as_bytes()), nonce)?;
376391
} else {
377392
decryption_ctx.decrypt_init(Some(self.cipher), Some(key_buf.as_bytes()), None)?;
378393
}
379394

380-
if ciphertext.len() < self.tag_len {
381-
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
382-
}
383-
384-
Ok(pyo3::types::PyBytes::new_with(
385-
py,
386-
ciphertext.len() - self.tag_len,
387-
|b| {
388-
EvpCipherAead::decrypt_with_context(
389-
decryption_ctx,
390-
ciphertext,
391-
aad,
392-
nonce,
393-
self.tag_len,
394-
self.tag_first,
395-
self.is_ccm,
396-
b,
397-
)?;
398-
Ok(())
399-
},
400-
)?)
395+
EvpCipherAead::decrypt_with_context(
396+
decryption_ctx,
397+
ciphertext,
398+
aad,
399+
nonce,
400+
self.tag_len,
401+
self.tag_first,
402+
self.is_ccm,
403+
buf,
404+
)
401405
}
402406
}
403407

@@ -474,6 +478,29 @@ impl EvpAead {
474478
},
475479
)?)
476480
}
481+
482+
fn decrypt_into(
483+
&self,
484+
_py: pyo3::Python<'_>,
485+
ciphertext: &[u8],
486+
aad: Option<Aad<'_>>,
487+
nonce: Option<&[u8]>,
488+
buf: &mut [u8],
489+
) -> CryptographyResult<()> {
490+
let ad = if let Some(Aad::Single(ad)) = &aad {
491+
check_length(ad.as_bytes())?;
492+
ad.as_bytes()
493+
} else {
494+
assert!(aad.is_none());
495+
b""
496+
};
497+
498+
self.ctx
499+
.decrypt(ciphertext, nonce.unwrap_or(b""), ad, buf)
500+
.map_err(|_| exceptions::InvalidTag::new_err(()))?;
501+
502+
Ok(())
503+
}
477504
}
478505

479506
#[pyo3::pyclass(frozen, module = "cryptography.hazmat.bindings._rust.openssl.aead")]
@@ -618,7 +645,36 @@ impl ChaCha20Poly1305 {
618645
data: CffiBuf<'_>,
619646
associated_data: Option<CffiBuf<'_>>,
620647
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
648+
if nonce.as_bytes().len() != 12 {
649+
return Err(CryptographyError::from(
650+
pyo3::exceptions::PyValueError::new_err("Nonce must be 12 bytes"),
651+
));
652+
}
653+
if data.as_bytes().len() < self.ctx.tag_len {
654+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
655+
}
656+
Ok(pyo3::types::PyBytes::new_with(
657+
py,
658+
data.as_bytes().len() - self.ctx.tag_len,
659+
|b| {
660+
let buf = CffiMutBuf::from_bytes(py, b);
661+
self.decrypt_into(py, nonce, data, associated_data, buf)?;
662+
Ok(())
663+
},
664+
)?)
665+
}
666+
667+
#[pyo3(signature = (nonce, data, associated_data, buf))]
668+
fn decrypt_into(
669+
&self,
670+
py: pyo3::Python<'_>,
671+
nonce: CffiBuf<'_>,
672+
data: CffiBuf<'_>,
673+
associated_data: Option<CffiBuf<'_>>,
674+
mut buf: CffiMutBuf<'_>,
675+
) -> CryptographyResult<usize> {
621676
let nonce_bytes = nonce.as_bytes();
677+
let data_bytes = data.as_bytes();
622678
let aad = associated_data.map(Aad::Single);
623679

624680
if nonce_bytes.len() != 12 {
@@ -627,8 +683,24 @@ impl ChaCha20Poly1305 {
627683
));
628684
}
629685

686+
if data.as_bytes().len() < self.ctx.tag_len {
687+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
688+
}
689+
690+
let expected_len = data_bytes.len() - self.ctx.tag_len;
691+
if buf.as_mut_bytes().len() != expected_len {
692+
return Err(CryptographyError::from(
693+
pyo3::exceptions::PyValueError::new_err(format!(
694+
"buffer must be {} bytes",
695+
expected_len
696+
)),
697+
));
698+
}
699+
630700
self.ctx
631-
.decrypt(py, data.as_bytes(), aad, Some(nonce_bytes))
701+
.decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
702+
703+
Ok(expected_len)
632704
}
633705
}
634706

tests/hazmat/primitives/test_aead.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,12 +118,20 @@ def test_nonce_not_12_bytes(self, backend):
118118
with pytest.raises(ValueError):
119119
chacha.decrypt(b"00", b"hello", b"")
120120

121+
with pytest.raises(ValueError):
122+
buf = bytearray(1)
123+
chacha.decrypt_into(b"00", b"hello", b"", buf)
124+
121125
def test_decrypt_data_too_short(self, backend):
122126
key = ChaCha20Poly1305.generate_key()
123127
chacha = ChaCha20Poly1305(key)
124128
with pytest.raises(InvalidTag):
125129
chacha.decrypt(b"0" * 12, b"0", None)
126130

131+
with pytest.raises(InvalidTag):
132+
buf = bytearray(16)
133+
chacha.decrypt_into(b"0" * 12, b"0", None, buf)
134+
127135
def test_associated_data_none_equal_to_empty_bytestring(self, backend):
128136
key = ChaCha20Poly1305.generate_key()
129137
chacha = ChaCha20Poly1305(key)
@@ -222,6 +230,44 @@ def test_encrypt_into_buffer_incorrect_size(self, ptlen, buflen, backend):
222230
with pytest.raises(ValueError, match="buffer must be"):
223231
chacha.encrypt_into(nonce, pt, None, buf)
224232

233+
def test_decrypt_into(self, backend):
234+
key = ChaCha20Poly1305.generate_key()
235+
chacha = ChaCha20Poly1305(key)
236+
nonce = os.urandom(12)
237+
pt = b"decrypt me"
238+
ad = b"additional"
239+
ct = chacha.encrypt(nonce, pt, ad)
240+
buf = bytearray(len(pt))
241+
n = chacha.decrypt_into(nonce, ct, ad, buf)
242+
assert n == len(pt)
243+
assert buf == pt
244+
245+
@pytest.mark.parametrize(
246+
("ctlen", "buflen"), [(26, 9), (26, 11), (31, 14), (36, 21)]
247+
)
248+
def test_decrypt_into_buffer_incorrect_size(self, ctlen, buflen, backend):
249+
key = ChaCha20Poly1305.generate_key()
250+
chacha = ChaCha20Poly1305(key)
251+
nonce = os.urandom(12)
252+
ct = b"x" * ctlen
253+
buf = bytearray(buflen)
254+
with pytest.raises(ValueError, match="buffer must be"):
255+
chacha.decrypt_into(nonce, ct, None, buf)
256+
257+
def test_decrypt_into_invalid_tag(self, backend):
258+
key = ChaCha20Poly1305.generate_key()
259+
chacha = ChaCha20Poly1305(key)
260+
nonce = os.urandom(12)
261+
pt = b"some data"
262+
ad = b"additional"
263+
ct = chacha.encrypt(nonce, pt, ad)
264+
# Corrupt the ciphertext
265+
corrupted_ct = bytearray(ct)
266+
corrupted_ct[0] ^= 1
267+
buf = bytearray(len(pt))
268+
with pytest.raises(InvalidTag):
269+
chacha.decrypt_into(nonce, bytes(corrupted_ct), ad, buf)
270+
225271

226272
@pytest.mark.skipif(
227273
not _aead_supported(AESCCM),

0 commit comments

Comments
 (0)