Skip to content

Commit d8975e2

Browse files
authored
implement decrypt_into for aesgcmsiv (#13788)
1 parent 47b7e95 commit d8975e2

File tree

5 files changed

+138
-67
lines changed

5 files changed

+138
-67
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Changelog
5050
* Added ``decrypt_into`` methods to
5151
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESCCM`,
5252
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESGCM`,
53+
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESGCMSIV`,
5354
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESOCB3`,
5455
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV`, and
5556
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to

docs/hazmat/primitives/aead.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -386,6 +386,33 @@ also support providing integrity for associated data which is not encrypted.
386386
when the ciphertext has been changed, but will also occur when the
387387
key, nonce, or associated data are wrong.
388388

389+
.. method:: decrypt_into(nonce, data, associated_data, buf)
390+
391+
.. versionadded:: 47.0.0
392+
393+
Decrypts the ``data`` and authenticates the ``associated_data``. If you
394+
called encrypt with ``associated_data`` you must pass the same
395+
``associated_data`` in decrypt or the integrity check will fail. The
396+
output is written into the ``buf`` parameter.
397+
398+
:param nonce: A 12-byte value.
399+
:type nonce: :term:`bytes-like`
400+
:param data: The data to decrypt (with tag appended).
401+
:type data: :term:`bytes-like`
402+
:param associated_data: Additional data to authenticate. Can be
403+
``None`` if none was passed during encryption.
404+
:type associated_data: :term:`bytes-like`
405+
:param buf: A writable :term:`bytes-like` object that must be exactly
406+
``len(data) - 16`` bytes. The plaintext will be written to this
407+
buffer.
408+
:returns int: The number of bytes written to the buffer (always
409+
``len(data) - 16``).
410+
:raises ValueError: If the buffer is not the correct size.
411+
:raises cryptography.exceptions.InvalidTag: If the authentication tag
412+
doesn't validate this exception will be raised. This will occur
413+
when the ciphertext has been changed, but will also occur when the
414+
key, nonce, or associated data are wrong.
415+
389416
.. class:: AESOCB3(key)
390417

391418
.. versionadded:: 36.0.0

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,10 @@ class AESGCMSIV:
180180
data: Buffer,
181181
associated_data: Buffer | None,
182182
) -> bytes: ...
183+
def decrypt_into(
184+
self,
185+
nonce: Buffer,
186+
data: Buffer,
187+
associated_data: Buffer | None,
188+
buf: Buffer,
189+
) -> int: ...

src/rust/src/backend/aead.rs

Lines changed: 53 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -192,40 +192,6 @@ impl EvpCipherAead {
192192
Ok(())
193193
}
194194

195-
#[cfg(not(any(CRYPTOGRAPHY_IS_BORINGSSL, CRYPTOGRAPHY_IS_AWSLC)))]
196-
fn decrypt<'p>(
197-
&self,
198-
py: pyo3::Python<'p>,
199-
ciphertext: &[u8],
200-
aad: Option<Aad<'_>>,
201-
nonce: Option<&[u8]>,
202-
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
203-
// Temporary while we remove this function
204-
if ciphertext.len() < self.tag_len {
205-
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
206-
}
207-
208-
let mut ctx = openssl::cipher_ctx::CipherCtx::new()?;
209-
ctx.copy(&self.base_decryption_ctx)?;
210-
Ok(pyo3::types::PyBytes::new_with(
211-
py,
212-
ciphertext.len() - self.tag_len,
213-
|b| {
214-
EvpCipherAead::decrypt_with_context(
215-
ctx,
216-
ciphertext,
217-
aad,
218-
nonce,
219-
self.tag_len,
220-
self.tag_first,
221-
false,
222-
b,
223-
)?;
224-
Ok(())
225-
},
226-
)?)
227-
}
228-
229195
fn decrypt_into(
230196
&self,
231197
// We have this arg so we have consistent arguments with decrypt_into in
@@ -428,38 +394,6 @@ impl EvpAead {
428394
Ok(())
429395
}
430396

431-
fn decrypt<'p>(
432-
&self,
433-
py: pyo3::Python<'p>,
434-
ciphertext: &[u8],
435-
aad: Option<Aad<'_>>,
436-
nonce: Option<&[u8]>,
437-
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
438-
if ciphertext.len() < self.tag_len {
439-
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
440-
}
441-
442-
let ad = if let Some(Aad::Single(ad)) = &aad {
443-
check_length(ad.as_bytes())?;
444-
ad.as_bytes()
445-
} else {
446-
assert!(aad.is_none());
447-
b""
448-
};
449-
450-
Ok(pyo3::types::PyBytes::new_with(
451-
py,
452-
ciphertext.len() - self.tag_len,
453-
|b| {
454-
self.ctx
455-
.decrypt(ciphertext, nonce.unwrap_or(b""), ad, b)
456-
.map_err(|_| exceptions::InvalidTag::new_err(()))?;
457-
458-
Ok(())
459-
},
460-
)?)
461-
}
462-
463397
fn decrypt_into(
464398
&self,
465399
_py: pyo3::Python<'_>,
@@ -1659,14 +1593,66 @@ impl AesGcmSiv {
16591593
associated_data: Option<CffiBuf<'_>>,
16601594
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
16611595
let nonce_bytes = nonce.as_bytes();
1596+
let data_bytes = data.as_bytes();
1597+
1598+
if nonce_bytes.len() != 12 {
1599+
return Err(CryptographyError::from(
1600+
pyo3::exceptions::PyValueError::new_err("Nonce must be 12 bytes long"),
1601+
));
1602+
}
1603+
1604+
if data_bytes.len() < self.ctx.tag_len {
1605+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
1606+
}
1607+
1608+
Ok(pyo3::types::PyBytes::new_with(
1609+
py,
1610+
data_bytes.len() - 16,
1611+
|b| {
1612+
let buf = CffiMutBuf::from_bytes(py, b);
1613+
self.decrypt_into(py, nonce, data, associated_data, buf)?;
1614+
Ok(())
1615+
},
1616+
)?)
1617+
}
1618+
1619+
#[pyo3(signature = (nonce, data, associated_data, buf))]
1620+
fn decrypt_into(
1621+
&self,
1622+
py: pyo3::Python<'_>,
1623+
nonce: CffiBuf<'_>,
1624+
data: CffiBuf<'_>,
1625+
associated_data: Option<CffiBuf<'_>>,
1626+
mut buf: CffiMutBuf<'_>,
1627+
) -> CryptographyResult<usize> {
1628+
let nonce_bytes = nonce.as_bytes();
1629+
let data_bytes = data.as_bytes();
16621630
let aad = associated_data.map(Aad::Single);
1631+
16631632
if nonce_bytes.len() != 12 {
16641633
return Err(CryptographyError::from(
16651634
pyo3::exceptions::PyValueError::new_err("Nonce must be 12 bytes long"),
16661635
));
16671636
}
1637+
1638+
if data_bytes.len() < self.ctx.tag_len {
1639+
return Err(CryptographyError::from(exceptions::InvalidTag::new_err(())));
1640+
}
1641+
1642+
let expected_len = data_bytes.len() - self.ctx.tag_len;
1643+
if buf.as_mut_bytes().len() != expected_len {
1644+
return Err(CryptographyError::from(
1645+
pyo3::exceptions::PyValueError::new_err(format!(
1646+
"buffer must be {} bytes",
1647+
expected_len
1648+
)),
1649+
));
1650+
}
1651+
16681652
self.ctx
1669-
.decrypt(py, data.as_bytes(), aad, Some(nonce_bytes))
1653+
.decrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
1654+
1655+
Ok(expected_len)
16701656
}
16711657
}
16721658

tests/hazmat/primitives/test_aead.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1287,6 +1287,10 @@ def test_invalid_nonce_length(self, backend):
12871287
with pytest.raises(ValueError):
12881288
aesgcmsiv.decrypt(nonce, pt, None)
12891289

1290+
with pytest.raises(ValueError):
1291+
buf = bytearray(16)
1292+
aesgcmsiv.decrypt_into(nonce, b"x" * 20, None, buf)
1293+
12901294
def test_empty(self):
12911295
key = AESGCMSIV.generate_key(256)
12921296
aesgcmsiv = AESGCMSIV(key)
@@ -1468,3 +1472,49 @@ def test_encrypt_into_buffer_incorrect_size(self, ptlen, buflen, backend):
14681472
buf = bytearray(buflen)
14691473
with pytest.raises(ValueError, match="buffer must be"):
14701474
aesgcmsiv.encrypt_into(nonce, pt, None, buf)
1475+
1476+
def test_decrypt_into(self, backend):
1477+
key = AESGCMSIV.generate_key(256)
1478+
aesgcmsiv = AESGCMSIV(key)
1479+
nonce = os.urandom(12)
1480+
pt = b"decrypt me"
1481+
ad = b"additional"
1482+
ct = aesgcmsiv.encrypt(nonce, pt, ad)
1483+
buf = bytearray(len(pt))
1484+
n = aesgcmsiv.decrypt_into(nonce, ct, ad, buf)
1485+
assert n == len(pt)
1486+
assert buf == pt
1487+
1488+
@pytest.mark.parametrize(
1489+
("ctlen", "buflen"), [(26, 9), (26, 11), (31, 14), (36, 21)]
1490+
)
1491+
def test_decrypt_into_buffer_incorrect_size(self, ctlen, buflen, backend):
1492+
key = AESGCMSIV.generate_key(256)
1493+
aesgcmsiv = AESGCMSIV(key)
1494+
nonce = os.urandom(12)
1495+
ct = b"x" * ctlen
1496+
buf = bytearray(buflen)
1497+
with pytest.raises(ValueError, match="buffer must be"):
1498+
aesgcmsiv.decrypt_into(nonce, ct, None, buf)
1499+
1500+
def test_decrypt_into_invalid_tag(self, backend):
1501+
key = AESGCMSIV.generate_key(256)
1502+
aesgcmsiv = AESGCMSIV(key)
1503+
nonce = os.urandom(12)
1504+
pt = b"some data"
1505+
ad = b"additional"
1506+
ct = aesgcmsiv.encrypt(nonce, pt, ad)
1507+
# Corrupt the ciphertext
1508+
corrupted_ct = bytearray(ct)
1509+
corrupted_ct[0] ^= 1
1510+
buf = bytearray(len(pt))
1511+
with pytest.raises(InvalidTag):
1512+
aesgcmsiv.decrypt_into(nonce, bytes(corrupted_ct), ad, buf)
1513+
1514+
def test_decrypt_into_data_too_short(self, backend):
1515+
key = AESGCMSIV.generate_key(256)
1516+
aesgcmsiv = AESGCMSIV(key)
1517+
nonce = os.urandom(12)
1518+
buf = bytearray(16)
1519+
with pytest.raises(InvalidTag):
1520+
aesgcmsiv.decrypt_into(nonce, b"short", None, buf)

0 commit comments

Comments
 (0)