diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 6de7a94d3cf4..29c2ba580667 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -49,8 +49,9 @@ Changelog :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`, :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDFExpand`, :class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`, - :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`, and - :class:`~cryptography.hazmat.primitives.kdf.scrypt.Scrypt` to allow + :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`, + :class:`~cryptography.hazmat.primitives.kdf.scrypt.Scrypt`, and + :class:`~cryptography.hazmat.primitives.kdf.x963kdf.X963KDF` to allow deriving keys directly into pre-allocated buffers. * Added ``encrypt_into`` methods to :class:`~cryptography.hazmat.primitives.ciphers.aead.AESCCM`, diff --git a/docs/hazmat/primitives/key-derivation-functions.rst b/docs/hazmat/primitives/key-derivation-functions.rst index 83e9fc9e963d..95cb62c46f6b 100644 --- a/docs/hazmat/primitives/key-derivation-functions.rst +++ b/docs/hazmat/primitives/key-derivation-functions.rst @@ -1269,13 +1269,40 @@ X963KDF :raises TypeError: This exception is raised if ``key_material`` is not ``bytes``. :raises cryptography.exceptions.AlreadyFinalized: This is raised when - :meth:`derive` or + :meth:`derive`, + :meth:`derive_into`, or :meth:`verify` is called more than once. Derives a new key from the input key material. + .. method:: derive_into(key_material, buffer) + + .. versionadded:: 47.0.0 + + :param key_material: The input key material. + :type key_material: :term:`bytes-like` + :param buffer: A writable buffer to write the derived key into. The + buffer must be equal to the length supplied in the + constructor. + :type buffer: :term:`bytes-like` + :return int: the number of bytes written to the buffer. + :raises ValueError: This exception is raised if the buffer length does + not match the specified ``length``. + :raises TypeError: This exception is raised if ``key_material`` or + ``buffer`` is not ``bytes``. + :raises cryptography.exceptions.AlreadyFinalized: This is raised when + :meth:`derive`, + :meth:`derive_into`, or + :meth:`verify` is + called more than + once. + + Derives a new key from the input key material and writes it into + the provided buffer. This is useful when you want to avoid allocating + new memory for the derived key. + .. method:: verify(key_material, expected_key) :param bytes key_material: The input key material. This is the same as @@ -1287,7 +1314,8 @@ X963KDF derived key does not match the expected key. :raises cryptography.exceptions.AlreadyFinalized: This is raised when - :meth:`derive` or + :meth:`derive`, + :meth:`derive_into`, or :meth:`verify` is called more than once. diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi index 408a9cb1c690..6437617ad29f 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi @@ -93,6 +93,7 @@ class X963KDF: backend: typing.Any = None, ) -> None: ... def derive(self, key_material: Buffer) -> bytes: ... + def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... def verify(self, key_material: bytes, expected_key: bytes) -> None: ... class ConcatKDFHash: diff --git a/src/rust/src/backend/kdf.rs b/src/rust/src/backend/kdf.rs index 4d329f9a6ac0..6cfa0aa82037 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -944,6 +944,55 @@ struct X963Kdf { used: bool, } +impl X963Kdf { + fn derive_into_buffer( + &mut self, + py: pyo3::Python<'_>, + key_material: &[u8], + output: &mut [u8], + ) -> CryptographyResult { + if self.used { + return Err(exceptions::already_finalized_error()); + } + self.used = true; + + if output.len() != self.length { + return Err(CryptographyError::from( + pyo3::exceptions::PyValueError::new_err(format!( + "buffer must be {} bytes", + self.length + )), + )); + } + + let algorithm_bound = self.algorithm.bind(py); + let digest_size = algorithm_bound + .getattr(pyo3::intern!(py, "digest_size"))? + .extract::()?; + + let mut pos = 0usize; + let mut counter = 1u32; + + while pos < self.length { + let mut hash_obj = hashes::Hash::new(py, algorithm_bound, None)?; + hash_obj.update_bytes(key_material)?; + hash_obj.update_bytes(&counter.to_be_bytes())?; + if let Some(ref sharedinfo) = self.sharedinfo { + hash_obj.update_bytes(sharedinfo.as_bytes(py))?; + } + let block = hash_obj.finalize(py)?; + let block_bytes = block.as_bytes(); + + let copy_len = (self.length - pos).min(digest_size); + output[pos..pos + copy_len].copy_from_slice(&block_bytes[..copy_len]); + pos += copy_len; + counter += 1; + } + + Ok(self.length) + } +} + #[pyo3::pymethods] impl X963Kdf { #[new] @@ -980,41 +1029,22 @@ impl X963Kdf { }) } + fn derive_into( + &mut self, + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + mut buf: CffiMutBuf<'_>, + ) -> CryptographyResult { + self.derive_into_buffer(py, key_material.as_bytes(), buf.as_mut_bytes()) + } + fn derive<'p>( &mut self, py: pyo3::Python<'p>, key_material: CffiBuf<'_>, ) -> CryptographyResult> { - if self.used { - return Err(exceptions::already_finalized_error()); - } - self.used = true; - - let algorithm_bound = self.algorithm.bind(py); - let digest_size = algorithm_bound - .getattr(pyo3::intern!(py, "digest_size"))? - .extract::()?; - Ok(pyo3::types::PyBytes::new_with(py, self.length, |output| { - let mut pos = 0usize; - let mut counter = 1u32; - - while pos < self.length { - let mut hash_obj = hashes::Hash::new(py, algorithm_bound, None)?; - hash_obj.update_bytes(key_material.as_bytes())?; - hash_obj.update_bytes(&counter.to_be_bytes())?; - if let Some(ref sharedinfo) = self.sharedinfo { - hash_obj.update_bytes(sharedinfo.as_bytes(py))?; - } - let block = hash_obj.finalize(py)?; - let block_bytes = block.as_bytes(); - - let copy_len = (self.length - pos).min(digest_size); - output[pos..pos + copy_len].copy_from_slice(&block_bytes[..copy_len]); - pos += copy_len; - counter += 1; - } - + self.derive_into_buffer(py, key_material.as_bytes(), output)?; Ok(()) })?) } diff --git a/tests/hazmat/primitives/test_x963kdf.py b/tests/hazmat/primitives/test_x963kdf.py index d276f6d00542..17c12e6a04cf 100644 --- a/tests/hazmat/primitives/test_x963kdf.py +++ b/tests/hazmat/primitives/test_x963kdf.py @@ -110,3 +110,32 @@ def test_unicode_typeerror(self, backend): ) xkdf.verify(b"foo", "bar") # type: ignore[arg-type] + + def test_derive_into(self, backend): + key = binascii.unhexlify( + b"96c05619d56c328ab95fe84b18264b08725b85e33fd34f08" + ) + xkdf = X963KDF(hashes.SHA256(), 16, None, backend) + buf = bytearray(16) + n = xkdf.derive_into(key, buf) + assert n == 16 + # Verify the output matches what derive would produce + xkdf2 = X963KDF(hashes.SHA256(), 16, None, backend) + expected = xkdf2.derive(key) + assert buf == expected + + @pytest.mark.parametrize( + ("buflen", "outlen"), [(15, 16), (17, 16), (8, 16), (32, 16)] + ) + def test_derive_into_buffer_incorrect_size(self, buflen, outlen, backend): + xkdf = X963KDF(hashes.SHA256(), outlen, None, backend) + buf = bytearray(buflen) + with pytest.raises(ValueError, match="buffer must be"): + xkdf.derive_into(b"key", buf) + + def test_derive_into_already_finalized(self, backend): + xkdf = X963KDF(hashes.SHA256(), 16, None, backend) + buf = bytearray(16) + xkdf.derive_into(b"key", buf) + with pytest.raises(AlreadyFinalized): + xkdf.derive_into(b"key", buf)