Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
42 changes: 36 additions & 6 deletions docs/hazmat/primitives/key-derivation-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand Down Expand Up @@ -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``.
Expand Down Expand Up @@ -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``.
Expand Down
1 change: 1 addition & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
68 changes: 51 additions & 17 deletions src/rust/src/backend/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<usize> {
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]
Expand Down Expand Up @@ -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<usize> {
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<pyo3::Bound<'p, pyo3::types::PyBytes>> {
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(())
})?)
}
Expand Down
38 changes: 38 additions & 0 deletions tests/hazmat/primitives/test_argon2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading