Skip to content

Commit 8b5ff0e

Browse files
authored
implement derive_into for scrypt (#13794)
1 parent a399be0 commit 8b5ff0e

File tree

5 files changed

+117
-18
lines changed

5 files changed

+117
-18
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,9 @@ Changelog
4848
* Added ``derive_into`` methods to
4949
:class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`,
5050
:class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDFExpand`,
51-
:class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`, and
52-
:class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC` to allow
51+
:class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`,
52+
:class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`, and
53+
:class:`~cryptography.hazmat.primitives.kdf.scrypt.Scrypt` to allow
5354
deriving keys directly into pre-allocated buffers.
5455
* Added ``encrypt_into`` methods to
5556
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESCCM`,

docs/hazmat/primitives/key-derivation-functions.rst

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -418,13 +418,40 @@ Scrypt
418418
:raises TypeError: This exception is raised if ``key_material`` is not
419419
``bytes``.
420420
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
421-
:meth:`derive` or
421+
:meth:`derive`,
422+
:meth:`derive_into`, or
422423
:meth:`verify` is
423424
called more than
424425
once.
425426

426427
This generates and returns a new key from the supplied password.
427428

429+
.. method:: derive_into(key_material, buffer)
430+
431+
.. versionadded:: 47.0.0
432+
433+
:param key_material: The input key material.
434+
:type key_material: :term:`bytes-like`
435+
:param buffer: A writable buffer to write the derived key into. The
436+
buffer must be equal to the length supplied in the
437+
constructor.
438+
:type buffer: :term:`bytes-like`
439+
:return int: the number of bytes written to the buffer.
440+
:raises ValueError: This exception is raised if the buffer length does
441+
not match the specified ``length``.
442+
:raises TypeError: This exception is raised if ``key_material`` or
443+
``buffer`` is not ``bytes``.
444+
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
445+
:meth:`derive`,
446+
:meth:`derive_into`, or
447+
:meth:`verify` is
448+
called more than
449+
once.
450+
451+
This generates a new key from the supplied password and writes it into
452+
the provided buffer. This is useful when you want to avoid allocating
453+
new memory for the derived key.
454+
428455
.. method:: verify(key_material, expected_key)
429456

430457
:param bytes key_material: The input key material. This is the same as
@@ -436,7 +463,8 @@ Scrypt
436463
derived key does not match
437464
the expected key.
438465
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
439-
:meth:`derive` or
466+
:meth:`derive`,
467+
:meth:`derive_into`, or
440468
:meth:`verify` is
441469
called more than
442470
once.

src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class Scrypt:
3131
backend: typing.Any = None,
3232
) -> None: ...
3333
def derive(self, key_material: Buffer) -> bytes: ...
34+
def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ...
3435
def verify(self, key_material: bytes, expected_key: bytes) -> None: ...
3536

3637
class Argon2id:

src/rust/src/backend/kdf.rs

Lines changed: 57 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,50 @@ struct Scrypt {
144144
used: bool,
145145
}
146146

147+
impl Scrypt {
148+
#[cfg(not(CRYPTOGRAPHY_IS_LIBRESSL))]
149+
fn derive_into_buffer(
150+
&mut self,
151+
py: pyo3::Python<'_>,
152+
key_material: &[u8],
153+
output: &mut [u8],
154+
) -> CryptographyResult<usize> {
155+
if self.used {
156+
return Err(exceptions::already_finalized_error());
157+
}
158+
self.used = true;
159+
160+
if output.len() != self.length {
161+
return Err(CryptographyError::from(
162+
pyo3::exceptions::PyValueError::new_err(format!(
163+
"buffer must be {} bytes",
164+
self.length
165+
)),
166+
));
167+
}
168+
169+
openssl::pkcs5::scrypt(
170+
key_material,
171+
self.salt.as_bytes(py),
172+
self.n,
173+
self.r,
174+
self.p,
175+
(usize::MAX / 2).try_into().unwrap(),
176+
output,
177+
)
178+
.map_err(|_| {
179+
// memory required formula explained here:
180+
// https://blog.filippo.io/the-scrypt-parameters/
181+
let min_memory = 128 * self.n * self.r / (1024 * 1024);
182+
CryptographyError::from(pyo3::exceptions::PyMemoryError::new_err(format!(
183+
"Not enough memory to derive key. These parameters require {min_memory}MB of memory."
184+
)))
185+
})?;
186+
187+
Ok(self.length)
188+
}
189+
}
190+
147191
#[pyo3::pymethods]
148192
impl Scrypt {
149193
#[new]
@@ -214,26 +258,25 @@ impl Scrypt {
214258
}
215259
}
216260

261+
#[cfg(not(CRYPTOGRAPHY_IS_LIBRESSL))]
262+
fn derive_into(
263+
&mut self,
264+
py: pyo3::Python<'_>,
265+
key_material: CffiBuf<'_>,
266+
mut buf: CffiMutBuf<'_>,
267+
) -> CryptographyResult<usize> {
268+
self.derive_into_buffer(py, key_material.as_bytes(), buf.as_mut_bytes())
269+
}
270+
217271
#[cfg(not(CRYPTOGRAPHY_IS_LIBRESSL))]
218272
fn derive<'p>(
219273
&mut self,
220274
py: pyo3::Python<'p>,
221275
key_material: CffiBuf<'_>,
222276
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
223-
if self.used {
224-
return Err(exceptions::already_finalized_error());
225-
}
226-
self.used = true;
227-
228-
Ok(pyo3::types::PyBytes::new_with(py, self.length, |b| {
229-
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(|_| {
230-
// memory required formula explained here:
231-
// https://blog.filippo.io/the-scrypt-parameters/
232-
let min_memory = 128 * self.n * self.r / (1024 * 1024);
233-
pyo3::exceptions::PyMemoryError::new_err(format!(
234-
"Not enough memory to derive key. These parameters require {min_memory}MB of memory."
235-
))
236-
})
277+
Ok(pyo3::types::PyBytes::new_with(py, self.length, |output| {
278+
self.derive_into_buffer(py, key_material.as_bytes(), output)?;
279+
Ok(())
237280
})?)
238281
}
239282

tests/hazmat/primitives/test_scrypt.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,29 @@ def test_invalid_r(self, backend):
233233
def test_invalid_p(self, backend):
234234
with pytest.raises(ValueError):
235235
Scrypt(b"NaCl", 64, 2, 8, 0, backend)
236+
237+
def test_derive_into(self, backend):
238+
scrypt = Scrypt(b"NaCl", 64, 1024, 8, 16, backend)
239+
buf = bytearray(64)
240+
n = scrypt.derive_into(b"password", buf)
241+
assert n == 64
242+
# Verify the output matches what derive would produce
243+
scrypt2 = Scrypt(b"NaCl", 64, 1024, 8, 16, backend)
244+
expected = scrypt2.derive(b"password")
245+
assert buf == expected
246+
247+
@pytest.mark.parametrize(
248+
("buflen", "outlen"), [(63, 64), (65, 64), (32, 64), (128, 64)]
249+
)
250+
def test_derive_into_buffer_incorrect_size(self, buflen, outlen, backend):
251+
scrypt = Scrypt(b"NaCl", outlen, 1024, 8, 16, backend)
252+
buf = bytearray(buflen)
253+
with pytest.raises(ValueError, match="buffer must be"):
254+
scrypt.derive_into(b"password", buf)
255+
256+
def test_derive_into_already_finalized(self, backend):
257+
scrypt = Scrypt(b"NaCl", 64, 1024, 8, 16, backend)
258+
buf = bytearray(64)
259+
scrypt.derive_into(b"password", buf)
260+
with pytest.raises(AlreadyFinalized):
261+
scrypt.derive_into(b"password", buf)

0 commit comments

Comments
 (0)