Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Changelog
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESCCM`,
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESGCM`,
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESGCMSIV`,
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESOCB3`,
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESSIV`, and
:class:`~cryptography.hazmat.primitives.ciphers.aead.ChaCha20Poly1305` to
allow encrypting directly into a pre-allocated buffer.
Expand Down
29 changes: 29 additions & 0 deletions docs/hazmat/primitives/aead.rst
Original file line number Diff line number Diff line change
Expand Up @@ -386,6 +386,35 @@ also support providing integrity for associated data which is not encrypted.
:raises OverflowError: If ``data`` or ``associated_data`` is larger
than 2\ :sup:`31` - 1 bytes.

.. method:: encrypt_into(nonce, data, associated_data, buf)

.. versionadded:: 47.0.0

.. warning::

Reuse of a ``nonce`` with a given ``key`` compromises the security
of any message with that ``nonce`` and ``key`` pair.

Encrypts and authenticates the ``data`` provided as well as
authenticating the ``associated_data``. The output is written into
the ``buf`` parameter.

:param nonce: A 12-15 byte value. **NEVER REUSE A NONCE** with a key.
:type nonce: :term:`bytes-like`
:param data: The data to encrypt.
:type data: :term:`bytes-like`
:param associated_data: Additional data that should be
authenticated with the key, but is not encrypted. Can be ``None``.
:type associated_data: :term:`bytes-like`
:param buf: A writable :term:`bytes-like` object that must be exactly
``len(data) + 16`` bytes. The ciphertext with the 16 byte tag
appended will be written to this buffer.
:returns int: The number of bytes written to the buffer (always
``len(data) + 16``).
:raises ValueError: If the buffer is not the correct size.
:raises OverflowError: If ``data`` or ``associated_data`` is larger
than 2\ :sup:`31` - 1 bytes.

.. method:: decrypt(nonce, data, associated_data)

Decrypts the ``data`` and authenticates the ``associated_data``. If you
Expand Down
7 changes: 7 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/aead.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ class AESOCB3:
data: Buffer,
associated_data: Buffer | None,
) -> bytes: ...
def encrypt_into(
self,
nonce: Buffer,
data: Buffer,
associated_data: Buffer | None,
buf: Buffer,
) -> int: ...
def decrypt(
self,
nonce: Buffer,
Expand Down
68 changes: 38 additions & 30 deletions src/rust/src/backend/aead.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,35 +127,6 @@ impl EvpCipherAead {
Ok(())
}

fn encrypt<'p>(
&self,
py: pyo3::Python<'p>,
plaintext: &[u8],
aad: Option<Aad<'_>>,
nonce: Option<&[u8]>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
let mut ctx = openssl::cipher_ctx::CipherCtx::new()?;
ctx.copy(&self.base_encryption_ctx)?;

Ok(pyo3::types::PyBytes::new_with(
py,
plaintext.len() + self.tag_len,
|b| {
Self::encrypt_with_context(
ctx,
plaintext,
aad,
nonce,
self.tag_len,
self.tag_first,
false,
b,
)?;
Ok(())
},
)?)
}

fn encrypt_into(
&self,
// We have this arg so we have consistent arguments with encrypt_into in
Expand Down Expand Up @@ -1171,7 +1142,30 @@ impl AesOcb3 {
data: CffiBuf<'_>,
associated_data: Option<CffiBuf<'_>>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
let data_bytes = data.as_bytes();
check_length(data_bytes)?;
Ok(pyo3::types::PyBytes::new_with(
py,
data_bytes.len() + 16,
|b| {
let buf = CffiMutBuf::from_bytes(py, b);
self.encrypt_into(py, nonce, data, associated_data, buf)?;
Ok(())
},
)?)
}

#[pyo3(signature = (nonce, data, associated_data, buf))]
fn encrypt_into(
&self,
py: pyo3::Python<'_>,
nonce: CffiBuf<'_>,
data: CffiBuf<'_>,
associated_data: Option<CffiBuf<'_>>,
mut buf: CffiMutBuf<'_>,
) -> CryptographyResult<usize> {
let nonce_bytes = nonce.as_bytes();
let data_bytes = data.as_bytes();
let aad = associated_data.map(Aad::Single);

if nonce_bytes.len() < 12 || nonce_bytes.len() > 15 {
Expand All @@ -1180,8 +1174,22 @@ impl AesOcb3 {
));
}

// Check this early so we know we can add tag_len without overflow
// check_length requires that the length be 2 ** 31 - 1 or smaller.
check_length(data_bytes)?;
let expected_len = data_bytes.len() + 16;
if buf.as_mut_bytes().len() != expected_len {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(format!(
"buffer must be {} bytes",
expected_len
)),
));
}

self.ctx
.encrypt(py, data.as_bytes(), aad, Some(nonce_bytes))
.encrypt_into(py, data_bytes, aad, Some(nonce_bytes), buf.as_mut_bytes())?;
Ok(expected_len)
}

#[pyo3(signature = (nonce, data, associated_data))]
Expand Down
31 changes: 31 additions & 0 deletions tests/hazmat/primitives/test_aead.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,13 @@ def test_invalid_nonce_length(self, backend):
with pytest.raises(ValueError):
aesocb3.encrypt(b"\x00" * 16, b"hi", None)

with pytest.raises(ValueError):
buf = bytearray(18)
aesocb3.encrypt_into(b"\x00" * 11, b"hi", None, buf)
with pytest.raises(ValueError):
buf = bytearray(18)
aesocb3.encrypt_into(b"\x00" * 16, b"hi", None, buf)

with pytest.raises(ValueError):
aesocb3.decrypt(b"\x00" * 11, b"hi", None)
with pytest.raises(ValueError):
Expand Down Expand Up @@ -789,6 +796,30 @@ def test_buffer_protocol(self, backend):
computed_pt2 = aesocb3_.decrypt(bytearray(nonce), ct2, ad)
assert computed_pt2 == pt

def test_encrypt_into(self, backend):
key = AESOCB3.generate_key(128)
aesocb3 = AESOCB3(key)
nonce = os.urandom(12)
pt = b"encrypt me"
ad = b"additional"
buf = bytearray(len(pt) + 16)
n = aesocb3.encrypt_into(nonce, pt, ad, buf)
assert n == len(pt) + 16
ct = aesocb3.encrypt(nonce, pt, ad)
assert buf == ct

@pytest.mark.parametrize(
("ptlen", "buflen"), [(10, 25), (10, 27), (15, 30), (20, 37)]
)
def test_encrypt_into_buffer_incorrect_size(self, ptlen, buflen, backend):
key = AESOCB3.generate_key(128)
aesocb3 = AESOCB3(key)
nonce = os.urandom(12)
pt = b"x" * ptlen
buf = bytearray(buflen)
with pytest.raises(ValueError, match="buffer must be"):
aesocb3.encrypt_into(nonce, pt, None, buf)


@pytest.mark.skipif(
not _aead_supported(AESSIV),
Expand Down