Skip to content

Commit

Permalink
argon2id support (#11524)
Browse files Browse the repository at this point in the history
* argon2id support

* make it all rust now

* set a threadpool number

* address comments

* set threadpool to max(available, current)

* review comments

* a few more improvements

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

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>

---------

Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
  • Loading branch information
reaperhulk and alex authored Nov 11, 2024
1 parent 8c32661 commit a7aa8ce
Show file tree
Hide file tree
Showing 9 changed files with 482 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ Changelog
* Relax the Authority Key Identifier requirements on root CA certificates
during X.509 verification to allow fields permitted by :rfc:`5280` but
forbidden by the CA/Browser BRs.
* Added support for :class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`
when using OpenSSL 3.2.0+.

.. _v43-0-3:

Expand Down
101 changes: 101 additions & 0 deletions docs/hazmat/primitives/key-derivation-functions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,106 @@ Different KDFs are suitable for different tasks such as:
Variable cost algorithms
~~~~~~~~~~~~~~~~~~~~~~~~

Argon2id
--------

.. currentmodule:: cryptography.hazmat.primitives.kdf.argon2

.. class:: Argon2id(*, salt, length, iterations, lanes, memory_cost, ad=None, secret=None)

.. versionadded:: 44.0.0

Argon2id is a KDF designed for password storage. It is designed to be
resistant to hardware attacks and is described in :rfc:`9106`.

This class conforms to the
:class:`~cryptography.hazmat.primitives.kdf.KeyDerivationFunction`
interface.

.. doctest::

>>> import os
>>> from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
>>> salt = os.urandom(16)
>>> # derive
>>> kdf = Argon2id(
... salt=salt,
... length=32,
... iterations=1,
... lanes=4,
... memory_cost=64 * 1024,
... ad=None,
... secret=None,
... )
>>> key = kdf.derive(b"my great password")
>>> # verify
>>> kdf = Argon2id(
... salt=salt,
... length=32,
... iterations=1,
... lanes=4,
... memory_cost=64 * 1024,
... ad=None,
... secret=None,
... )
>>> kdf.verify(b"my great password", key)

**All arguments to the constructor are keyword-only.**

:param bytes salt: A salt should be unique (and randomly generated) per
password and is recommended to be 16 bytes or longer
:param int length: The desired length of the derived key in bytes.
:param int iterations: Also known as passes, this is used to tune
the running time independently of the memory size.
:param int lanes: The number of lanes (parallel threads) to use. Also
known as parallelism.
:param int memory_cost: The amount of memory to use in kibibytes.
1 kibibyte (KiB) is 1024 bytes. This must be at minimum ``8 * lanes``.
:param bytes ad: Optional associated data.
:param bytes secret: Optional secret data; used for keyed hashing.

:rfc:`9106` has recommendations for `parameter choice`_.

:raises cryptography.exceptions.UnsupportedAlgorithm: If Argon2id is not
supported by the OpenSSL version ``cryptography`` is using.

.. method:: derive(key_material)

:param key_material: The input key material.
:type key_material: :term:`bytes-like`
:return bytes: the derived key.
: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
called more than
once.

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

.. method:: verify(key_material, expected_key)

:param bytes key_material: The input key material. This is the same as
``key_material`` in :meth:`derive`.
:param bytes expected_key: The expected result of deriving a new key,
this is the same as the return value of
:meth:`derive`.
:raises cryptography.exceptions.InvalidKey: This is raised when the
derived key does not match
the expected key.
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
:meth:`derive` or
:meth:`verify` is
called more than
once.

This checks whether deriving a new key from the supplied
``key_material`` generates the same key as the ``expected_key``, and
raises an exception if they do not match. This can be used for
checking whether the password a user provides matches the stored derived
key.


PBKDF2
------
Expand Down Expand Up @@ -1039,3 +1139,4 @@ Interface
.. _`recommends`: https://datatracker.ietf.org/doc/html/rfc7914#section-2
.. _`The scrypt paper`: https://www.tarsnap.com/scrypt/scrypt.pdf
.. _`understanding HKDF`: https://soatok.blog/2021/11/17/understanding-hkdf/
.. _`parameter choice`: https://datatracker.ietf.org/doc/html/rfc9106#section-4
3 changes: 3 additions & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ iOS
iterable
Kerberos
Keychain
KiB
kibibyte
kibibytes
Koblitz
Lange
logins
Expand Down
6 changes: 6 additions & 0 deletions src/cryptography/hazmat/backends/openssl/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,12 @@ def scrypt_supported(self) -> bool:
else:
return hasattr(rust_openssl.kdf.Scrypt, "derive")

def argon2_supported(self) -> bool:
if self._fips_enabled:
return False
else:
return hasattr(rust_openssl.kdf.Argon2id, "derive")

def hmac_supported(self, algorithm: hashes.HashAlgorithm) -> bool:
# FIPS mode still allows SHA1 for HMAC
if self._fips_enabled and isinstance(algorithm, hashes.SHA1):
Expand Down
15 changes: 15 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,18 @@ class Scrypt:
) -> None: ...
def derive(self, key_material: bytes) -> bytes: ...
def verify(self, key_material: bytes, expected_key: bytes) -> None: ...

class Argon2id:
def __init__(
self,
*,
salt: bytes,
length: int,
iterations: int,
lanes: int,
memory_cost: int,
ad: bytes | None = None,
secret: bytes | None = None,
) -> None: ...
def derive(self, key_material: bytes) -> bytes: ...
def verify(self, key_material: bytes, expected_key: bytes) -> None: ...
13 changes: 13 additions & 0 deletions src/cryptography/hazmat/primitives/kdf/argon2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# This file is dual licensed under the terms of the Apache License, Version
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
# for complete details.

from __future__ import annotations

from cryptography.hazmat.bindings._rust import openssl as rust_openssl
from cryptography.hazmat.primitives.kdf import KeyDerivationFunction

Argon2id = rust_openssl.kdf.Argon2id
KeyDerivationFunction.register(Argon2id)

__all__ = ["Argon2id"]
168 changes: 168 additions & 0 deletions src/rust/src/backend/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,10 +164,178 @@ impl Scrypt {
}
}

#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")]
struct Argon2id {
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
salt: pyo3::Py<pyo3::types::PyBytes>,
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
length: usize,
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
iterations: u32,
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
lanes: u32,
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
memory_cost: u32,
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
ad: Option<pyo3::Py<pyo3::types::PyBytes>>,
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
secret: Option<pyo3::Py<pyo3::types::PyBytes>>,
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
used: bool,
}

#[pyo3::pymethods]
impl Argon2id {
#[new]
#[pyo3(signature = (salt, length, iterations, lanes, memory_cost, ad=None, secret=None))]
#[allow(clippy::too_many_arguments)]
fn new(
py: pyo3::Python<'_>,
salt: pyo3::Py<pyo3::types::PyBytes>,
length: usize,
iterations: u32,
lanes: u32,
memory_cost: u32,
ad: Option<pyo3::Py<pyo3::types::PyBytes>>,
secret: Option<pyo3::Py<pyo3::types::PyBytes>>,
) -> CryptographyResult<Self> {
cfg_if::cfg_if! {
if #[cfg(not(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER))] {
_ = py;
_ = salt;
_ = length;
_ = iterations;
_ = lanes;
_ = memory_cost;
_ = ad;
_ = secret;

Err(CryptographyError::from(
exceptions::UnsupportedAlgorithm::new_err(
"This version of OpenSSL does not support argon2id"
),
))
} else {
if cryptography_openssl::fips::is_enabled() {
return Err(CryptographyError::from(
exceptions::UnsupportedAlgorithm::new_err(
"This version of OpenSSL does not support argon2id"
),
));
}

if salt.as_bytes(py).len() < 8 {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"salt must be at least 8 bytes"
),
));
}
if length < 4 {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"length must be greater than or equal to 4."
),
));
}
if iterations < 1 {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"iterations must be greater than or equal to 1."
),
));
}
if lanes < 1 {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"lanes must be greater than or equal to 1."
),
));
}

if memory_cost / 8 < lanes {
return Err(CryptographyError::from(
pyo3::exceptions::PyValueError::new_err(
"memory_cost must be an integer >= 8 * lanes."
),
));
}


Ok(Argon2id{
salt,
length,
iterations,
lanes,
memory_cost,
ad,
secret,
used: false,
})
}
}
}

#[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_bound_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(())
},
)?)
}

#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
fn verify(
&mut self,
py: pyo3::Python<'_>,
key_material: CffiBuf<'_>,
expected_key: CffiBuf<'_>,
) -> CryptographyResult<()> {
let actual = self.derive(py, key_material)?;
let actual_bytes = actual.as_bytes();
let expected_bytes = expected_key.as_bytes();

if actual_bytes.len() != expected_bytes.len()
|| !openssl::memcmp::eq(actual_bytes, expected_bytes)
{
return Err(CryptographyError::from(exceptions::InvalidKey::new_err(
"Keys do not match.",
)));
}

Ok(())
}
}

#[pyo3::pymodule]
pub(crate) mod kdf {
#[pymodule_export]
use super::derive_pbkdf2_hmac;
#[pymodule_export]
use super::Argon2id;
#[pymodule_export]
use super::Scrypt;
}
14 changes: 14 additions & 0 deletions src/rust/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,20 @@ mod _rust {
openssl_mod.add("_legacy_provider_loaded", false)?;
}
}
cfg_if::cfg_if! {
if #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] {
use std::ptr;
use std::cmp::max;

let available = std::thread::available_parallelism().map_or(0, |v| v.get() as u64);
// SAFETY: This sets a libctx provider limit, but we always use the same libctx by passing NULL.
unsafe {
let current = openssl_sys::OSSL_get_max_threads(ptr::null_mut());
// Set the thread limit to the max of available parallelism or current limit.
openssl_sys::OSSL_set_max_threads(ptr::null_mut(), max(available, current));
}
}
}

Ok(())
}
Expand Down
Loading

0 comments on commit a7aa8ce

Please sign in to comment.