Skip to content

Commit 41dacaf

Browse files
committed
implement derive_into for pbkdf2hmac
1 parent d084c0e commit 41dacaf

File tree

5 files changed

+106
-33
lines changed

5 files changed

+106
-33
lines changed

CHANGELOG.rst

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,9 @@ Changelog
4747
private implementation will be removed in 49.0.0.
4848
* Added ``derive_into`` methods to
4949
:class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`,
50-
:class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDFExpand`, and
51-
:class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id` to allow
50+
: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
5253
deriving keys directly into pre-allocated buffers.
5354
* Added ``encrypt_into`` methods to
5455
:class:`~cryptography.hazmat.primitives.ciphers.aead.AESCCM`,

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

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -282,8 +282,9 @@ PBKDF2
282282
:type key_material: :term:`bytes-like`
283283
:return bytes: the derived key.
284284
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
285-
:meth:`derive` or
286-
:meth:`verify` is
285+
:meth:`derive`,
286+
:meth:`derive_into`,
287+
or :meth:`verify` is
287288
called more than
288289
once.
289290

@@ -292,6 +293,31 @@ PBKDF2
292293

293294
This generates and returns a new key from the supplied password.
294295

296+
.. method:: derive_into(key_material, buffer)
297+
298+
.. versionadded:: 47.0.0
299+
300+
:param key_material: The input key material. For PBKDF2 this
301+
should be a password.
302+
:type key_material: :term:`bytes-like`
303+
:param buffer: A writable buffer to write the derived key into. The
304+
buffer must be equal to the length supplied in the
305+
constructor.
306+
:return int: The number of bytes written to the buffer.
307+
:raises TypeError: This exception is raised if ``key_material`` is not
308+
``bytes``.
309+
:raises ValueError: This exception is raised if the buffer is too small
310+
for the derived key.
311+
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
312+
:meth:`derive`,
313+
:meth:`derive_into`,
314+
or :meth:`verify` is
315+
called more than
316+
once.
317+
318+
This generates a new key from the supplied password and writes it
319+
directly into the provided buffer.
320+
295321
.. method:: verify(key_material, expected_key)
296322

297323
:param bytes key_material: The input key material. This is the same as
@@ -303,8 +329,9 @@ PBKDF2
303329
derived key does not match
304330
the expected key.
305331
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
306-
:meth:`derive` or
307-
:meth:`verify` is
332+
:meth:`derive`,
333+
:meth:`derive_into`,
334+
or :meth:`verify` is
308335
called more than
309336
once.
310337

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class PBKDF2HMAC:
1717
backend: typing.Any = None,
1818
) -> None: ...
1919
def derive(self, key_material: Buffer) -> bytes: ...
20+
def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ...
2021
def verify(self, key_material: bytes, expected_key: bytes) -> None: ...
2122

2223
class Scrypt:

src/rust/src/backend/kdf.rs

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -15,20 +15,6 @@ use crate::buf::{CffiBuf, CffiMutBuf};
1515
use crate::error::{CryptographyError, CryptographyResult};
1616
use crate::exceptions;
1717

18-
pub(crate) fn pbkdf2_hmac_derive<'p>(
19-
py: pyo3::Python<'p>,
20-
key_material: &[u8],
21-
md: openssl::hash::MessageDigest,
22-
salt: &[u8],
23-
iterations: usize,
24-
length: usize,
25-
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
26-
Ok(pyo3::types::PyBytes::new_with(py, length, |b| {
27-
openssl::pkcs5::pbkdf2_hmac(key_material, salt, iterations, md, b).unwrap();
28-
Ok(())
29-
})?)
30-
}
31-
3218
// NO-COVERAGE-START
3319
#[pyo3::pyclass(
3420
module = "cryptography.hazmat.primitives.kdf.pbkdf2",
@@ -43,6 +29,40 @@ struct Pbkdf2Hmac {
4329
used: bool,
4430
}
4531

32+
impl Pbkdf2Hmac {
33+
fn derive_into_buffer(
34+
&mut self,
35+
py: pyo3::Python<'_>,
36+
key_material: &[u8],
37+
output: &mut [u8],
38+
) -> CryptographyResult<usize> {
39+
if self.used {
40+
return Err(exceptions::already_finalized_error());
41+
}
42+
self.used = true;
43+
44+
if output.len() != self.length {
45+
return Err(CryptographyError::from(
46+
pyo3::exceptions::PyValueError::new_err(format!(
47+
"buffer must be {} bytes",
48+
self.length
49+
)),
50+
));
51+
}
52+
53+
openssl::pkcs5::pbkdf2_hmac(
54+
key_material,
55+
self.salt.as_bytes(py),
56+
self.iterations,
57+
self.md,
58+
output,
59+
)
60+
.unwrap();
61+
62+
Ok(self.length)
63+
}
64+
}
65+
4666
#[pyo3::pymethods]
4767
impl Pbkdf2Hmac {
4868
#[new]
@@ -67,24 +87,24 @@ impl Pbkdf2Hmac {
6787
})
6888
}
6989

90+
fn derive_into(
91+
&mut self,
92+
py: pyo3::Python<'_>,
93+
key_material: CffiBuf<'_>,
94+
mut buf: CffiMutBuf<'_>,
95+
) -> CryptographyResult<usize> {
96+
self.derive_into_buffer(py, key_material.as_bytes(), buf.as_mut_bytes())
97+
}
98+
7099
fn derive<'p>(
71100
&mut self,
72101
py: pyo3::Python<'p>,
73102
key_material: CffiBuf<'_>,
74103
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
75-
if self.used {
76-
return Err(exceptions::already_finalized_error());
77-
}
78-
self.used = true;
79-
80-
pbkdf2_hmac_derive(
81-
py,
82-
key_material.as_bytes(),
83-
self.md,
84-
self.salt.as_bytes(py),
85-
self.iterations,
86-
self.length,
87-
)
104+
Ok(pyo3::types::PyBytes::new_with(py, self.length, |output| {
105+
self.derive_into_buffer(py, key_material.as_bytes(), output)?;
106+
Ok(())
107+
})?)
88108
}
89109

90110
fn verify(

tests/hazmat/primitives/test_pbkdf2hmac.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,27 @@ def test_buffer_protocol(self, backend):
6161
kdf = PBKDF2HMAC(hashes.SHA1(), 10, b"salt", 10, backend)
6262
data = bytearray(b"data")
6363
assert kdf.derive(data) == b"\xe9n\xaa\x81\xbbt\xa4\xf6\x08\xce"
64+
65+
def test_derive_into(self, backend):
66+
kdf = PBKDF2HMAC(hashes.SHA1(), 20, b"salt", 10, backend)
67+
buf = bytearray(20)
68+
n = kdf.derive_into(b"password", buf)
69+
assert n == 20
70+
# Verify the output matches what derive would produce
71+
kdf2 = PBKDF2HMAC(hashes.SHA1(), 20, b"salt", 10, backend)
72+
expected = kdf2.derive(b"password")
73+
assert buf == expected
74+
75+
@pytest.mark.parametrize(("buflen", "outlen"), [(19, 20), (21, 20)])
76+
def test_derive_into_buffer_incorrect_size(self, buflen, outlen, backend):
77+
kdf = PBKDF2HMAC(hashes.SHA1(), outlen, b"salt", 10, backend)
78+
buf = bytearray(buflen)
79+
with pytest.raises(ValueError, match="buffer must be"):
80+
kdf.derive_into(b"password", buf)
81+
82+
def test_derive_into_already_finalized(self, backend):
83+
kdf = PBKDF2HMAC(hashes.SHA1(), 20, b"salt", 10, backend)
84+
buf = bytearray(20)
85+
kdf.derive_into(b"password", buf)
86+
with pytest.raises(AlreadyFinalized):
87+
kdf.derive_into(b"password2", buf)

0 commit comments

Comments
 (0)