diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0187f9818be7..6de7a94d3cf4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -48,8 +48,9 @@ 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.argon2.Argon2id`, and - :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC` to allow + :class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`, + :class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`, and + :class:`~cryptography.hazmat.primitives.kdf.scrypt.Scrypt` 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 2219299884bd..83e9fc9e963d 100644 --- a/docs/hazmat/primitives/key-derivation-functions.rst +++ b/docs/hazmat/primitives/key-derivation-functions.rst @@ -418,13 +418,40 @@ Scrypt :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. 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. + :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. + + This generates a new key from the supplied password 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 @@ -436,7 +463,8 @@ Scrypt 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 c4cfebaab6b7..408a9cb1c690 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi @@ -31,6 +31,7 @@ class Scrypt: 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 Argon2id: diff --git a/src/rust/src/backend/kdf.rs b/src/rust/src/backend/kdf.rs index a8320f94dcae..4d329f9a6ac0 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -144,6 +144,50 @@ struct Scrypt { used: bool, } +impl Scrypt { + #[cfg(not(CRYPTOGRAPHY_IS_LIBRESSL))] + 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::pkcs5::scrypt( + key_material, + self.salt.as_bytes(py), + self.n, + self.r, + self.p, + (usize::MAX / 2).try_into().unwrap(), + output, + ) + .map_err(|_| { + // memory required formula explained here: + // https://blog.filippo.io/the-scrypt-parameters/ + let min_memory = 128 * self.n * self.r / (1024 * 1024); + CryptographyError::from(pyo3::exceptions::PyMemoryError::new_err(format!( + "Not enough memory to derive key. These parameters require {min_memory}MB of memory." + ))) + })?; + + Ok(self.length) + } +} + #[pyo3::pymethods] impl Scrypt { #[new] @@ -214,26 +258,25 @@ impl Scrypt { } } + #[cfg(not(CRYPTOGRAPHY_IS_LIBRESSL))] + 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(not(CRYPTOGRAPHY_IS_LIBRESSL))] 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::pkcs5::scrypt(key_material.as_bytes(), self.salt.as_bytes(py), self.n, self.r, self.p, (usize::MAX / 2).try_into().unwrap(), b).map_err(|_| { - // memory required formula explained here: - // https://blog.filippo.io/the-scrypt-parameters/ - let min_memory = 128 * self.n * self.r / (1024 * 1024); - pyo3::exceptions::PyMemoryError::new_err(format!( - "Not enough memory to derive key. These parameters require {min_memory}MB of memory." - )) - }) + 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_scrypt.py b/tests/hazmat/primitives/test_scrypt.py index 4b4641854755..27be0436165e 100644 --- a/tests/hazmat/primitives/test_scrypt.py +++ b/tests/hazmat/primitives/test_scrypt.py @@ -233,3 +233,29 @@ def test_invalid_r(self, backend): def test_invalid_p(self, backend): with pytest.raises(ValueError): Scrypt(b"NaCl", 64, 2, 8, 0, backend) + + def test_derive_into(self, backend): + scrypt = Scrypt(b"NaCl", 64, 1024, 8, 16, backend) + buf = bytearray(64) + n = scrypt.derive_into(b"password", buf) + assert n == 64 + # Verify the output matches what derive would produce + scrypt2 = Scrypt(b"NaCl", 64, 1024, 8, 16, backend) + expected = scrypt2.derive(b"password") + assert buf == expected + + @pytest.mark.parametrize( + ("buflen", "outlen"), [(63, 64), (65, 64), (32, 64), (128, 64)] + ) + def test_derive_into_buffer_incorrect_size(self, buflen, outlen, backend): + scrypt = Scrypt(b"NaCl", outlen, 1024, 8, 16, backend) + buf = bytearray(buflen) + with pytest.raises(ValueError, match="buffer must be"): + scrypt.derive_into(b"password", buf) + + def test_derive_into_already_finalized(self, backend): + scrypt = Scrypt(b"NaCl", 64, 1024, 8, 16, backend) + buf = bytearray(64) + scrypt.derive_into(b"password", buf) + with pytest.raises(AlreadyFinalized): + scrypt.derive_into(b"password", buf)