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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ 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`.

.. _v43-0-0:

Expand Down
100 changes: 100 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,105 @@ 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.
: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.
:param bytes ad: Optional associated data.
:param bytes secret: Optional secret data.

: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 +1138,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, "derive_scrypt")

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

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
10 changes: 10 additions & 0 deletions src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,13 @@ def derive_scrypt(
max_mem: int,
length: int,
) -> bytes: ...
def derive_argon2id(
key_material: bytes,
salt: bytes,
length: int,
iterations: int,
lanes: int,
memory_cost: int,
ad: bytes | None,
secret: bytes | None,
) -> bytes: ...
87 changes: 87 additions & 0 deletions src/cryptography/hazmat/primitives/kdf/argon2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# 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 import utils
from cryptography.exceptions import (
AlreadyFinalized,
InvalidKey,
UnsupportedAlgorithm,
)
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
from cryptography.hazmat.primitives import constant_time
from cryptography.hazmat.primitives.kdf import KeyDerivationFunction


class Argon2id(KeyDerivationFunction):
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason we shouldn't just make this whole class be rust?

def __init__(
self,
*,
salt: bytes,
length: int,
iterations: int,
lanes: int,
memory_cost: int,
ad: bytes | None = None,
secret: bytes | None = None,
):
from cryptography.hazmat.backends.openssl.backend import (
backend as ossl,
)

if not ossl.argon2_supported():
raise UnsupportedAlgorithm(
"This version of OpenSSL does not support argon2id"
)

utils._check_bytes("salt", salt)
# OpenSSL requires a salt of at least 8 bytes
if len(salt) < 8:
raise ValueError("salt must be at least 8 bytes")
# Minimum length is 4 bytes as specified in RFC 9106
if not isinstance(length, int) or length < 4:
raise ValueError("length must be an integer greater >= 4")
if not isinstance(iterations, int) or iterations < 1:
raise ValueError("iterations must be an integer greater than 0")
if not isinstance(lanes, int) or lanes < 1:
raise ValueError("lanes must be an integer greater than 0")
# Memory cost must be at least 8 * lanes
if not isinstance(memory_cost, int) or memory_cost < 8 * lanes:
raise ValueError("memory_cost must be an integer >= 8 * lanes")
if ad is not None:
utils._check_bytes("ad", ad)
if secret is not None:
utils._check_bytes("secret", secret)

self._used = False
self._salt = salt
self._length = length
self._iterations = iterations
self._lanes = lanes
self._memory_cost = memory_cost
self._ad = ad
self._secret = secret

def derive(self, key_material: bytes) -> bytes:
if self._used:
raise AlreadyFinalized("argon2id instances can only be used once.")
self._used = True

utils._check_byteslike("key_material", key_material)

return rust_openssl.kdf.derive_argon2id(
key_material,
self._salt,
self._length,
self._iterations,
self._lanes,
self._memory_cost,
self._ad,
self._secret,
)

def verify(self, key_material: bytes, expected_key: bytes) -> None:
if not constant_time.bytes_eq(self.derive(key_material), expected_key):
raise InvalidKey
9 changes: 3 additions & 6 deletions src/rust/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions src/rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ cryptography-x509 = { path = "cryptography-x509" }
cryptography-x509-verification = { path = "cryptography-x509-verification" }
cryptography-openssl = { path = "cryptography-openssl" }
pem = { version = "3", default-features = false }
openssl = "0.10.66"
openssl-sys = "0.9.103"
openssl = { git = "https://github.com/sfackler/rust-openssl" }
openssl-sys = { git = "https://github.com/sfackler/rust-openssl" }
foreign-types-shared = "0.1"
self_cell = "1"

Expand Down
2 changes: 1 addition & 1 deletion src/rust/cryptography-cffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ rust-version.workspace = true

[dependencies]
pyo3 = { version = "0.22.2", features = ["abi3"] }
openssl-sys = "0.9.103"
openssl-sys = { git = "https://github.com/sfackler/rust-openssl" }

[build-dependencies]
cc = "1.1.15"
4 changes: 2 additions & 2 deletions src/rust/cryptography-key-parsing/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ rust-version.workspace = true
[dependencies]
asn1 = { version = "0.17.0", default-features = false }
cfg-if = "1"
openssl = "0.10.66"
openssl-sys = "0.9.103"
openssl = { git = "https://github.com/sfackler/rust-openssl" }
openssl-sys = { git = "https://github.com/sfackler/rust-openssl" }
cryptography-x509 = { path = "../cryptography-x509" }
4 changes: 2 additions & 2 deletions src/rust/cryptography-openssl/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ rust-version.workspace = true

[dependencies]
cfg-if = "1"
openssl = "0.10.66"
ffi = { package = "openssl-sys", version = "0.9.101" }
openssl = { git = "https://github.com/sfackler/rust-openssl" }
ffi = { package = "openssl-sys", git = "https://github.com/sfackler/rust-openssl" }
foreign-types = "0.3"
foreign-types-shared = "0.1"
36 changes: 36 additions & 0 deletions src/rust/src/backend/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,44 @@ fn derive_scrypt<'p>(
})?)
}

#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
#[pyo3::pyfunction]
#[allow(clippy::too_many_arguments)]
#[pyo3(signature = (key_material, salt, length, iterations, lanes, memory_cost, ad=None, secret=None))]
fn derive_argon2id<'p>(
py: pyo3::Python<'p>,
key_material: CffiBuf<'_>,
salt: &[u8],
length: usize,
iterations: u32,
lanes: u32,
memory_cost: u32,
ad: Option<&[u8]>,
secret: Option<&[u8]>,
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
use crate::error::CryptographyError;

Ok(pyo3::types::PyBytes::new_bound_with(py, length, |b| {
openssl::kdf::argon2id(
key_material.as_bytes(),
salt,
ad,
secret,
iterations,
lanes,
memory_cost,
b,
)
.map_err(CryptographyError::from)?;
Ok(())
})?)
}

#[pyo3::pymodule]
pub(crate) mod kdf {
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
#[pymodule_export]
use super::derive_argon2id;
#[pymodule_export]
use super::derive_pbkdf2_hmac;
#[cfg(not(CRYPTOGRAPHY_IS_LIBRESSL))]
Expand Down
Loading