Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

argon2id support #11524

Merged
merged 8 commits into from
Nov 11, 2024
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
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
Comment on lines +126 to +127
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any decade now...

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

reaperhulk marked this conversation as resolved.
Show resolved Hide resolved
Argon2id = rust_openssl.kdf.Argon2id
reaperhulk marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -221,6 +221,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));
alex marked this conversation as resolved.
Show resolved Hide resolved
}
reaperhulk marked this conversation as resolved.
Show resolved Hide resolved
}
}

Ok(())
}
Expand Down
Loading