diff --git a/CHANGELOG.rst b/CHANGELOG.rst index ad11a5510a37..2b780fb7a0e6 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -46,8 +46,9 @@ Changelog to :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`. The previous private implementation will be removed in 49.0.0. * Added ``derive_into`` methods to - :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF` and - :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDFExpand` to allow + :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`, + :class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDFExpand`, and + :class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id` 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 07d62b1682c4..66ede78ced8e 100644 --- a/docs/hazmat/primitives/key-derivation-functions.rst +++ b/docs/hazmat/primitives/key-derivation-functions.rst @@ -101,13 +101,38 @@ Argon2id :raises TypeError: This exception is raised if ``key_material`` is not ``bytes``. :raises cryptography.exceptions.AlreadyFinalized: This is raised when - :meth:`derive` or - :meth:`verify` is + :meth:`derive`, + :meth:`derive_into`, + or :meth:`verify` is called more than once. This generates and returns a new key from the supplied password. + .. 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. + :return int: The number of bytes written to the buffer. + :raises TypeError: This exception is raised if ``key_material`` is not + ``bytes``. + :raises ValueError: This exception is raised if the buffer is too small + for the derived key. + :raises cryptography.exceptions.AlreadyFinalized: This is raised when + :meth:`derive`, + :meth:`derive_into`, + or :meth:`verify` is + called more than + once. + + This generates a new key from the supplied password and writes it + directly into the provided buffer. + .. method:: verify(key_material, expected_key) :param bytes key_material: The input key material. This is the same as @@ -119,8 +144,9 @@ Argon2id derived key does not match the expected key. :raises cryptography.exceptions.AlreadyFinalized: This is raised when - :meth:`derive` or - :meth:`verify` is + :meth:`derive`, + :meth:`derive_into`, + or :meth:`verify` is called more than once. @@ -680,7 +706,9 @@ HKDF :param key_material: The input key material. :type key_material: :term:`bytes-like` - :param buffer: A writable buffer to write the derived key into. + :param buffer: A writable buffer to write the derived key into. The + buffer must be equal to the length supplied in the + constructor. :return int: The number of bytes written to the buffer. :raises TypeError: This exception is raised if ``key_material`` is not ``bytes``. @@ -787,7 +815,9 @@ HKDF .. versionadded:: 47.0.0 :param bytes key_material: The input key material. - :param buffer: A writable buffer to write the derived key into. + :param buffer: A writable buffer to write the derived key into. The + buffer must be equal to the length supplied in the + constructor. :return int: The number of bytes written to the buffer. :raises TypeError: This exception is raised if ``key_material`` is not ``bytes``. diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi index 00b530bb241e..bb96f25af5dd 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi @@ -45,6 +45,7 @@ class Argon2id: secret: bytes | None = None, ) -> None: ... def derive(self, key_material: bytes) -> bytes: ... + def derive_into(self, key_material: bytes, buffer: Buffer) -> int: ... def verify(self, key_material: bytes, expected_key: bytes) -> None: ... def derive_phc_encoded(self, key_material: bytes) -> str: ... @classmethod diff --git a/src/rust/src/backend/kdf.rs b/src/rust/src/backend/kdf.rs index 579a773a2df4..8d2e64bc909d 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -258,6 +258,45 @@ struct Argon2id { used: bool, } +impl Argon2id { + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + 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 + )), + )); + } + + openssl::kdf::argon2id( + None, + key_material, + self.salt.as_bytes(py), + self.ad.as_ref().map(|ad| ad.as_bytes(py)), + self.secret.as_ref().map(|secret| secret.as_bytes(py)), + self.iterations, + self.lanes, + self.memory_cost, + output, + ) + .map_err(CryptographyError::from)?; + + Ok(self.length) + } +} + #[pyo3::pymethods] impl Argon2id { #[new] @@ -350,29 +389,24 @@ impl Argon2id { } } + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + 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()) + } + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] 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; - Ok(pyo3::types::PyBytes::new_with(py, self.length, |b| { - openssl::kdf::argon2id( - None, - key_material.as_bytes(), - self.salt.as_bytes(py), - self.ad.as_ref().map(|ad| ad.as_bytes(py)), - self.secret.as_ref().map(|secret| secret.as_bytes(py)), - self.iterations, - self.lanes, - self.memory_cost, - b, - ) - .map_err(CryptographyError::from)?; + Ok(pyo3::types::PyBytes::new_with(py, self.length, |output| { + self.derive_into_buffer(py, key_material.as_bytes(), output)?; Ok(()) })?) } diff --git a/tests/hazmat/primitives/test_argon2.py b/tests/hazmat/primitives/test_argon2.py index 8db9c8cf0c92..ef2cc0cedd96 100644 --- a/tests/hazmat/primitives/test_argon2.py +++ b/tests/hazmat/primitives/test_argon2.py @@ -160,6 +160,44 @@ def test_verify(self, backend): salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 ).verify(b"password", digest) + def test_derive_into(self, backend): + argon2id = Argon2id( + salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 + ) + buf = bytearray(32) + n = argon2id.derive_into(b"password", buf) + assert n == 32 + # Verify the output matches what derive would produce + argon2id2 = Argon2id( + salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 + ) + expected = argon2id2.derive(b"password") + assert buf == expected + + @pytest.mark.parametrize( + ("buflen", "outlen"), [(31, 32), (33, 32), (16, 32), (64, 32)] + ) + def test_derive_into_buffer_incorrect_size(self, buflen, outlen, backend): + argon2id = Argon2id( + salt=b"salt" * 2, + length=outlen, + iterations=1, + lanes=1, + memory_cost=32, + ) + buf = bytearray(buflen) + with pytest.raises(ValueError, match="buffer must be"): + argon2id.derive_into(b"password", buf) + + def test_derive_into_already_finalized(self, backend): + argon2id = Argon2id( + salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 + ) + buf = bytearray(32) + argon2id.derive_into(b"password", buf) + with pytest.raises(AlreadyFinalized): + argon2id.derive_into(b"password2", buf) + def test_derive_phc_encoded(self, backend): # Test that we can generate a PHC formatted string argon2id = Argon2id(