From 0d361d6215babdef3350cd91d43a14552a469777 Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Sat, 1 Nov 2025 14:51:40 -0700 Subject: [PATCH] implement encrypt_into on aesocb3 --- CHANGELOG.rst | 1 + docs/hazmat/primitives/aead.rst | 29 ++++++++ .../hazmat/bindings/_rust/openssl/aead.pyi | 7 ++ src/rust/src/backend/aead.rs | 68 +++++++++++-------- tests/hazmat/primitives/test_aead.py | 31 +++++++++ 5 files changed, 106 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b8996046ca27..90a6705168a5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -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. diff --git a/docs/hazmat/primitives/aead.rst b/docs/hazmat/primitives/aead.rst index 353ed765b7e8..5ba59faad613 100644 --- a/docs/hazmat/primitives/aead.rst +++ b/docs/hazmat/primitives/aead.rst @@ -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 diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/aead.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/aead.pyi index 72325a603eca..08300450e1fa 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/aead.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/aead.pyi @@ -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, diff --git a/src/rust/src/backend/aead.rs b/src/rust/src/backend/aead.rs index 452dea72d071..80ab0698302f 100644 --- a/src/rust/src/backend/aead.rs +++ b/src/rust/src/backend/aead.rs @@ -127,35 +127,6 @@ impl EvpCipherAead { Ok(()) } - fn encrypt<'p>( - &self, - py: pyo3::Python<'p>, - plaintext: &[u8], - aad: Option>, - nonce: Option<&[u8]>, - ) -> CryptographyResult> { - 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 @@ -1171,7 +1142,30 @@ impl AesOcb3 { data: CffiBuf<'_>, associated_data: Option>, ) -> CryptographyResult> { + 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>, + mut buf: CffiMutBuf<'_>, + ) -> CryptographyResult { 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 { @@ -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))] diff --git a/tests/hazmat/primitives/test_aead.py b/tests/hazmat/primitives/test_aead.py index 01d44664e92a..3ec0d7e0121e 100644 --- a/tests/hazmat/primitives/test_aead.py +++ b/tests/hazmat/primitives/test_aead.py @@ -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): @@ -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),