diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 29c2ba580667..b1fc07a5da97 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -48,6 +48,7 @@ Changelog * Added ``derive_into`` methods to :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`, :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDFExpand`, + :class:`~cryptography.hazmat.primitives.kdf.concatkdf.ConcatKDFHash`, :class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`, :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`, :class:`~cryptography.hazmat.primitives.kdf.scrypt.Scrypt`, and diff --git a/docs/hazmat/primitives/key-derivation-functions.rst b/docs/hazmat/primitives/key-derivation-functions.rst index 95cb62c46f6b..528c1ad098f5 100644 --- a/docs/hazmat/primitives/key-derivation-functions.rst +++ b/docs/hazmat/primitives/key-derivation-functions.rst @@ -535,13 +535,40 @@ ConcatKDF :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 diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi index 6437617ad29f..17090850f171 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi @@ -105,6 +105,7 @@ class ConcatKDFHash: 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 ConcatKDFHMAC: diff --git a/src/rust/src/backend/kdf.rs b/src/rust/src/backend/kdf.rs index 6cfa0aa82037..43267fc0c8ab 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -1082,6 +1082,55 @@ struct ConcatKdfHash { used: bool, } +impl ConcatKdfHash { + 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(&counter.to_be_bytes())?; + hash_obj.update_bytes(key_material)?; + if let Some(ref otherinfo) = self.otherinfo { + hash_obj.update_bytes(otherinfo.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 ConcatKdfHash { #[new] @@ -1117,41 +1166,22 @@ impl ConcatKdfHash { }) } + 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(&counter.to_be_bytes())?; - hash_obj.update_bytes(key_material.as_bytes())?; - if let Some(ref otherinfo) = self.otherinfo { - hash_obj.update_bytes(otherinfo.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_concatkdf.py b/tests/hazmat/primitives/test_concatkdf.py index f0c44fbba556..f4d10d570c59 100644 --- a/tests/hazmat/primitives/test_concatkdf.py +++ b/tests/hazmat/primitives/test_concatkdf.py @@ -125,6 +125,39 @@ def test_unicode_typeerror(self, backend): ckdf.verify(b"foo", "bar") # type: ignore[arg-type] + def test_derive_into(self, backend): + prk = binascii.unhexlify( + b"52169af5c485dcc2321eb8d26d5efa21fb9b93c98e38412ee2484cf14f0d0d23" + ) + oinfo = binascii.unhexlify( + b"a1b2c3d4e53728157e634612c12d6d5223e204aeea4341565369647bd184bcd2" + b"46f72971f292badaa2fe4124612cba" + ) + ckdf = ConcatKDFHash(hashes.SHA256(), 16, oinfo, backend) + buf = bytearray(16) + n = ckdf.derive_into(prk, buf) + assert n == 16 + # Verify the output matches what derive would produce + ckdf2 = ConcatKDFHash(hashes.SHA256(), 16, oinfo, backend) + expected = ckdf2.derive(prk) + 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): + ckdf = ConcatKDFHash(hashes.SHA256(), outlen, None, backend) + buf = bytearray(buflen) + with pytest.raises(ValueError, match="buffer must be"): + ckdf.derive_into(b"key", buf) + + def test_derive_into_already_finalized(self, backend): + ckdf = ConcatKDFHash(hashes.SHA256(), 16, None, backend) + buf = bytearray(16) + ckdf.derive_into(b"key", buf) + with pytest.raises(AlreadyFinalized): + ckdf.derive_into(b"key", buf) + class TestConcatKDFHMAC: def test_length_limit(self, backend):