Skip to content

Commit 3edc452

Browse files
authored
implement derive_into on concatkdfhmac (#13799)
1 parent 0d70284 commit 3edc452

File tree

5 files changed

+127
-31
lines changed

5 files changed

+127
-31
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Changelog
4949
:class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDF`,
5050
:class:`~cryptography.hazmat.primitives.kdf.hkdf.HKDFExpand`,
5151
:class:`~cryptography.hazmat.primitives.kdf.concatkdf.ConcatKDFHash`,
52+
:class:`~cryptography.hazmat.primitives.kdf.concatkdf.ConcatKDFHMAC`,
5253
:class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`,
5354
:class:`~cryptography.hazmat.primitives.kdf.pbkdf2.PBKDF2HMAC`,
5455
:class:`~cryptography.hazmat.primitives.kdf.scrypt.Scrypt`, and

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

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -580,7 +580,8 @@ ConcatKDF
580580
derived key does not match
581581
the expected key.
582582
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
583-
:meth:`derive` or
583+
:meth:`derive`,
584+
:meth:`derive_into`, or
584585
:meth:`verify` is
585586
called more than
586587
once.
@@ -649,13 +650,40 @@ ConcatKDF
649650
:raises TypeError: This exception is raised if ``key_material`` is not
650651
``bytes``.
651652
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
652-
:meth:`derive` or
653+
:meth:`derive`,
654+
:meth:`derive_into`, or
653655
:meth:`verify` is
654656
called more than
655657
once.
656658

657659
Derives a new key from the input key material.
658660

661+
.. method:: derive_into(key_material, buffer)
662+
663+
.. versionadded:: 47.0.0
664+
665+
:param key_material: The input key material.
666+
:type key_material: :term:`bytes-like`
667+
:param buffer: A writable buffer to write the derived key into. The
668+
buffer must be equal to the length supplied in the
669+
constructor.
670+
:type buffer: :term:`bytes-like`
671+
:return int: the number of bytes written to the buffer.
672+
:raises ValueError: This exception is raised if the buffer length does
673+
not match the specified ``length``.
674+
:raises TypeError: This exception is raised if ``key_material`` or
675+
``buffer`` is not ``bytes``.
676+
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
677+
:meth:`derive`,
678+
:meth:`derive_into`, or
679+
:meth:`verify` is
680+
called more than
681+
once.
682+
683+
Derives a new key from the input key material and writes it into
684+
the provided buffer. This is useful when you want to avoid allocating
685+
new memory for the derived key.
686+
659687
.. method:: verify(key_material, expected_key)
660688

661689
:param bytes key_material: The input key material. This is the same as
@@ -667,7 +695,8 @@ ConcatKDF
667695
derived key does not match
668696
the expected key.
669697
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
670-
:meth:`derive` or
698+
:meth:`derive`,
699+
:meth:`derive_into`, or
671700
:meth:`verify` is
672701
called more than
673702
once.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,5 @@ class ConcatKDFHMAC:
118118
backend: typing.Any = None,
119119
) -> None: ...
120120
def derive(self, key_material: Buffer) -> bytes: ...
121+
def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ...
121122
def verify(self, key_material: bytes, expected_key: bytes) -> None: ...

src/rust/src/backend/kdf.rs

Lines changed: 58 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,54 @@ struct ConcatKdfHmac {
12201220
used: bool,
12211221
}
12221222

1223+
impl ConcatKdfHmac {
1224+
fn derive_into_buffer(
1225+
&mut self,
1226+
py: pyo3::Python<'_>,
1227+
key_material: &[u8],
1228+
output: &mut [u8],
1229+
) -> CryptographyResult<usize> {
1230+
if self.used {
1231+
return Err(exceptions::already_finalized_error());
1232+
}
1233+
self.used = true;
1234+
1235+
if output.len() != self.length {
1236+
return Err(CryptographyError::from(
1237+
pyo3::exceptions::PyValueError::new_err(format!(
1238+
"buffer must be {} bytes",
1239+
self.length
1240+
)),
1241+
));
1242+
}
1243+
1244+
let algorithm_bound = self.algorithm.bind(py);
1245+
let digest_size = algorithm_bound
1246+
.getattr(pyo3::intern!(py, "digest_size"))?
1247+
.extract::<usize>()?;
1248+
1249+
let mut pos = 0usize;
1250+
let mut counter = 1u32;
1251+
1252+
while pos < self.length {
1253+
let mut hmac = Hmac::new_bytes(py, self.salt.as_bytes(py), algorithm_bound)?;
1254+
hmac.update_bytes(&counter.to_be_bytes())?;
1255+
hmac.update_bytes(key_material)?;
1256+
if let Some(ref otherinfo) = self.otherinfo {
1257+
hmac.update_bytes(otherinfo.as_bytes(py))?;
1258+
}
1259+
let result = hmac.finalize_bytes()?;
1260+
1261+
let copy_len = (self.length - pos).min(digest_size);
1262+
output[pos..pos + copy_len].copy_from_slice(&result[..copy_len]);
1263+
pos += copy_len;
1264+
counter += 1;
1265+
}
1266+
1267+
Ok(self.length)
1268+
}
1269+
}
1270+
12231271
#[pyo3::pymethods]
12241272
impl ConcatKdfHmac {
12251273
#[new]
@@ -1278,40 +1326,22 @@ impl ConcatKdfHmac {
12781326
})
12791327
}
12801328

1329+
fn derive_into(
1330+
&mut self,
1331+
py: pyo3::Python<'_>,
1332+
key_material: CffiBuf<'_>,
1333+
mut buf: CffiMutBuf<'_>,
1334+
) -> CryptographyResult<usize> {
1335+
self.derive_into_buffer(py, key_material.as_bytes(), buf.as_mut_bytes())
1336+
}
1337+
12811338
fn derive<'p>(
12821339
&mut self,
12831340
py: pyo3::Python<'p>,
12841341
key_material: CffiBuf<'_>,
12851342
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
1286-
if self.used {
1287-
return Err(exceptions::already_finalized_error());
1288-
}
1289-
self.used = true;
1290-
1291-
let algorithm_bound = self.algorithm.bind(py);
1292-
let digest_size = algorithm_bound
1293-
.getattr(pyo3::intern!(py, "digest_size"))?
1294-
.extract::<usize>()?;
1295-
12961343
Ok(pyo3::types::PyBytes::new_with(py, self.length, |output| {
1297-
let mut pos = 0usize;
1298-
let mut counter = 1u32;
1299-
1300-
while pos < self.length {
1301-
let mut hmac = Hmac::new_bytes(py, self.salt.as_bytes(py), algorithm_bound)?;
1302-
hmac.update_bytes(&counter.to_be_bytes())?;
1303-
hmac.update_bytes(key_material.as_bytes())?;
1304-
if let Some(ref otherinfo) = self.otherinfo {
1305-
hmac.update_bytes(otherinfo.as_bytes(py))?;
1306-
}
1307-
let result = hmac.finalize_bytes()?;
1308-
1309-
let copy_len = (self.length - pos).min(digest_size);
1310-
output[pos..pos + copy_len].copy_from_slice(&result[..copy_len]);
1311-
pos += copy_len;
1312-
counter += 1;
1313-
}
1314-
1344+
self.derive_into_buffer(py, key_material.as_bytes(), output)?;
13151345
Ok(())
13161346
})?)
13171347
}

tests/hazmat/primitives/test_concatkdf.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,3 +324,38 @@ def test_unsupported_hash_algorithm(self, backend):
324324
otherinfo=None,
325325
backend=backend,
326326
)
327+
328+
def test_derive_into(self, backend):
329+
prk = binascii.unhexlify(
330+
b"013951627c1dea63ea2d7702dd24e963eef5faac6b4af7e4"
331+
b"b831cde499dff1ce45f6179f741c728aa733583b02409208"
332+
b"8f0af7fce1d045edbc5790931e8d5ca79c73"
333+
)
334+
oinfo = binascii.unhexlify(
335+
b"a1b2c3d4e55e600be5f367e0e8a465f4bf2704db00c9325c"
336+
b"9fbd216d12b49160b2ae5157650f43415653696421e68e"
337+
)
338+
ckdf = ConcatKDFHMAC(hashes.SHA512(), 32, None, oinfo, backend)
339+
buf = bytearray(32)
340+
n = ckdf.derive_into(prk, buf)
341+
assert n == 32
342+
# Verify the output matches what derive would produce
343+
ckdf2 = ConcatKDFHMAC(hashes.SHA512(), 32, None, oinfo, backend)
344+
expected = ckdf2.derive(prk)
345+
assert buf == expected
346+
347+
@pytest.mark.parametrize(
348+
("buflen", "outlen"), [(31, 32), (33, 32), (16, 32), (64, 32)]
349+
)
350+
def test_derive_into_buffer_incorrect_size(self, buflen, outlen, backend):
351+
ckdf = ConcatKDFHMAC(hashes.SHA512(), outlen, None, None, backend)
352+
buf = bytearray(buflen)
353+
with pytest.raises(ValueError, match="buffer must be"):
354+
ckdf.derive_into(b"key", buf)
355+
356+
def test_derive_into_already_finalized(self, backend):
357+
ckdf = ConcatKDFHMAC(hashes.SHA512(), 32, None, None, backend)
358+
buf = bytearray(32)
359+
ckdf.derive_into(b"key", buf)
360+
with pytest.raises(AlreadyFinalized):
361+
ckdf.derive_into(b"key", buf)

0 commit comments

Comments
 (0)