Skip to content

Commit 4b0205a

Browse files
authored
implement decrypt_into on aessiv (#13782)
1 parent 390ded0 commit 4b0205a

File tree

5 files changed

+185
-27
lines changed

5 files changed

+185
-27
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ 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.
5053

5154
.. _v46-0-3:
5255

docs/hazmat/primitives/aead.rst

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,31 @@ also support providing integrity for associated data which is not encrypted.
559559
when the ciphertext has been changed, but will also occur when the
560560
key or associated data are wrong.
561561

562+
.. method:: decrypt_into(data, associated_data, buf)
563+
564+
.. versionadded:: 47.0.0
565+
566+
Decrypts the ``data`` and authenticates the ``associated_data``. If you
567+
called encrypt with ``associated_data`` you must pass the same
568+
``associated_data`` in decrypt or the integrity check will fail. The
569+
output is written into the ``buf`` parameter.
570+
571+
:param bytes data: The data to decrypt (with tag **prepended**).
572+
:param list associated_data: An optional ``list`` of ``bytes-like objects``. This
573+
is additional data that should be authenticated with the key, but
574+
is not encrypted. Can be ``None`` if none was used during
575+
encryption.
576+
:param buf: A writable :term:`bytes-like` object that must be exactly
577+
``len(data) - 16`` bytes. The plaintext will be written to this
578+
buffer.
579+
:returns int: The number of bytes written to the buffer (always
580+
``len(data) - 16``).
581+
:raises ValueError: If the buffer is not the correct size.
582+
:raises cryptography.exceptions.InvalidTag: If the authentication tag
583+
doesn't validate this exception will be raised. This will occur
584+
when the ciphertext has been changed, but will also occur when the
585+
key or associated data are wrong.
586+
562587
.. class:: AESCCM(key, tag_length=16)
563588

564589
.. versionadded:: 2.0

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ class AESSIV:
9898
data: Buffer,
9999
associated_data: Sequence[Buffer] | None,
100100
) -> bytes: ...
101+
def decrypt_into(
102+
self,
103+
data: Buffer,
104+
associated_data: Sequence[Buffer] | None,
105+
buf: Buffer,
106+
) -> int: ...
101107

102108
class AESOCB3:
103109
def __init__(self, key: Buffer) -> None: ...

src/rust/src/backend/aead.rs

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -199,35 +199,68 @@ impl EvpCipherAead {
199199
aad: Option<Aad<'_>>,
200200
nonce: Option<&[u8]>,
201201
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
202+
// Temporary while we remove this function
203+
if ciphertext.len() < self.tag_len {
204+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
205+
}
206+
202207
let mut ctx = openssl::cipher_ctx::CipherCtx::new()?;
203208
ctx.copy(&self.base_decryption_ctx)?;
204-
Self::decrypt_with_context(
209+
Ok(pyo3::types::PyBytes::new_with(
205210
py,
211+
ciphertext.len() - self.tag_len,
212+
|b| {
213+
EvpCipherAead::decrypt_with_context(
214+
ctx,
215+
ciphertext,
216+
aad,
217+
nonce,
218+
self.tag_len,
219+
self.tag_first,
220+
false,
221+
b,
222+
)?;
223+
Ok(())
224+
},
225+
)?)
226+
}
227+
228+
fn decrypt_into(
229+
&self,
230+
// We have this arg so we have consistent arguments with decrypt_into in
231+
// LazyEvpCipherAead. We can remove it when we remove LazyEvpCipherAead.
232+
_py: pyo3::Python<'_>,
233+
ciphertext: &[u8],
234+
aad: Option<Aad<'_>>,
235+
nonce: Option<&[u8]>,
236+
buf: &mut [u8],
237+
) -> CryptographyResult<()> {
238+
let mut ctx = openssl::cipher_ctx::CipherCtx::new()?;
239+
ctx.copy(&self.base_decryption_ctx)?;
240+
241+
Self::decrypt_with_context(
206242
ctx,
207243
ciphertext,
208244
aad,
209245
nonce,
210246
self.tag_len,
211247
self.tag_first,
212248
false,
249+
buf,
213250
)
214251
}
215252

216253
#[allow(clippy::too_many_arguments)]
217-
fn decrypt_with_context<'p>(
218-
py: pyo3::Python<'p>,
254+
fn decrypt_with_context(
219255
mut ctx: openssl::cipher_ctx::CipherCtx,
220256
ciphertext: &[u8],
221257
aad: Option<Aad<'_>>,
222258
nonce: Option<&[u8]>,
223259
tag_len: usize,
224260
tag_first: bool,
225261
is_ccm: bool,
226-
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
227-
if ciphertext.len() < tag_len {
228-
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
229-
}
230-
262+
buf: &mut [u8],
263+
) -> CryptographyResult<()> {
231264
let tag;
232265
let ciphertext_data;
233266
if tag_first {
@@ -253,16 +286,10 @@ impl EvpCipherAead {
253286

254287
Self::process_aad(&mut ctx, aad)?;
255288

256-
Ok(pyo3::types::PyBytes::new_with(
257-
py,
258-
ciphertext_data.len(),
259-
|b| {
260-
Self::process_data(&mut ctx, ciphertext_data, b, is_ccm)
261-
.map_err(|_| exceptions::InvalidTag::new_err(()))?;
289+
Self::process_data(&mut ctx, ciphertext_data, buf, is_ccm)
290+
.map_err(|_| exceptions::InvalidTag::new_err(()))?;
262291

263-
Ok(())
264-
},
265-
)?)
292+
Ok(())
266293
}
267294
}
268295

@@ -350,16 +377,27 @@ impl LazyEvpCipherAead {
350377
decryption_ctx.decrypt_init(Some(self.cipher), Some(key_buf.as_bytes()), None)?;
351378
}
352379

353-
EvpCipherAead::decrypt_with_context(
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(
354385
py,
355-
decryption_ctx,
356-
ciphertext,
357-
aad,
358-
nonce,
359-
self.tag_len,
360-
self.tag_first,
361-
self.is_ccm,
362-
)
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+
)?)
363401
}
364402
}
365403

@@ -1060,8 +1098,50 @@ impl AesSiv {
10601098
data: CffiBuf<'_>,
10611099
associated_data: Option<pyo3::Bound<'_, pyo3::types::PyList>>,
10621100
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
1101+
if data.as_bytes().len() < self.ctx.tag_len {
1102+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
1103+
}
1104+
Ok(pyo3::types::PyBytes::new_with(
1105+
py,
1106+
data.as_bytes().len() - self.ctx.tag_len,
1107+
|b| {
1108+
let buf = CffiMutBuf::from_bytes(py, b);
1109+
self.decrypt_into(py, data, associated_data, buf)?;
1110+
Ok(())
1111+
},
1112+
)?)
1113+
}
1114+
1115+
#[pyo3(signature = (data, associated_data, buf))]
1116+
fn decrypt_into(
1117+
&self,
1118+
py: pyo3::Python<'_>,
1119+
data: CffiBuf<'_>,
1120+
associated_data: Option<pyo3::Bound<'_, pyo3::types::PyList>>,
1121+
mut buf: CffiMutBuf<'_>,
1122+
) -> CryptographyResult<usize> {
1123+
let data_bytes = data.as_bytes();
10631124
let aad = associated_data.map(Aad::List);
1064-
self.ctx.decrypt(py, data.as_bytes(), aad, None)
1125+
1126+
// We need to do this check early to prevent underflow when computing expected_len
1127+
if data_bytes.len() < self.ctx.tag_len {
1128+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
1129+
}
1130+
1131+
let expected_len = data_bytes.len() - self.ctx.tag_len;
1132+
if buf.as_mut_bytes().len() != expected_len {
1133+
return Err(CryptographyError::from(
1134+
pyo3::exceptions::PyValueError::new_err(format!(
1135+
"buffer must be {} bytes",
1136+
expected_len
1137+
)),
1138+
));
1139+
}
1140+
1141+
self.ctx
1142+
.decrypt_into(py, data_bytes, aad, None, buf.as_mut_bytes())?;
1143+
1144+
Ok(expected_len)
10651145
}
10661146
}
10671147

tests/hazmat/primitives/test_aead.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -953,6 +953,15 @@ def test_bad_generate_key(self, backend):
953953
with pytest.raises(ValueError):
954954
AESSIV.generate_key(128)
955955

956+
def test_data_too_short(self, backend):
957+
key = AESSIV.generate_key(256)
958+
aessiv = AESSIV(key)
959+
with pytest.raises(InvalidTag):
960+
aessiv.decrypt(b"tooshort", None)
961+
with pytest.raises(InvalidTag):
962+
buf = bytearray(16)
963+
aessiv.decrypt_into(b"tooshort", None, buf)
964+
956965
def test_associated_data_none_equal_to_empty_list(self, backend):
957966
key = AESSIV.generate_key(256)
958967
aessiv = AESSIV(key)
@@ -999,6 +1008,41 @@ def test_encrypt_into_buffer_incorrect_size(self, ptlen, buflen, backend):
9991008
with pytest.raises(ValueError, match="buffer must be"):
10001009
aessiv.encrypt_into(pt, None, buf)
10011010

1011+
def test_decrypt_into(self, backend):
1012+
key = AESSIV.generate_key(256)
1013+
aessiv = AESSIV(key)
1014+
pt = b"decrypt me"
1015+
ad = [b"additional"]
1016+
ct = aessiv.encrypt(pt, ad)
1017+
buf = bytearray(len(pt))
1018+
n = aessiv.decrypt_into(ct, ad, buf)
1019+
assert n == len(pt)
1020+
assert buf == pt
1021+
1022+
@pytest.mark.parametrize(
1023+
("ctlen", "buflen"), [(26, 9), (26, 11), (31, 14), (36, 21)]
1024+
)
1025+
def test_decrypt_into_buffer_incorrect_size(self, ctlen, buflen, backend):
1026+
key = AESSIV.generate_key(256)
1027+
aessiv = AESSIV(key)
1028+
ct = b"x" * ctlen
1029+
buf = bytearray(buflen)
1030+
with pytest.raises(ValueError, match="buffer must be"):
1031+
aessiv.decrypt_into(ct, None, buf)
1032+
1033+
def test_decrypt_into_invalid_tag(self, backend):
1034+
key = AESSIV.generate_key(256)
1035+
aessiv = AESSIV(key)
1036+
pt = b"some data"
1037+
ad = [b"additional"]
1038+
ct = aessiv.encrypt(pt, ad)
1039+
# Corrupt the ciphertext
1040+
corrupted_ct = bytearray(ct)
1041+
corrupted_ct[0] ^= 1
1042+
buf = bytearray(len(pt))
1043+
with pytest.raises(InvalidTag):
1044+
aessiv.decrypt_into(bytes(corrupted_ct), ad, buf)
1045+
10021046

10031047
@pytest.mark.skipif(
10041048
not _aead_supported(AESGCMSIV),

0 commit comments

Comments
 (0)