Skip to content

Commit

Permalink
argon2id support
Browse files Browse the repository at this point in the history
  • Loading branch information
reaperhulk committed Sep 2, 2024
1 parent 408b9f8 commit be36e9b
Show file tree
Hide file tree
Showing 13 changed files with 399 additions and 13 deletions.
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
98 changes: 98 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,103 @@ 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)

: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 +1136,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: ...
86 changes: 86 additions & 0 deletions src/cryptography/hazmat/primitives/kdf/argon2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# 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):
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 ossl.openssl_version_number() < 0x30200000:
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"
39 changes: 39 additions & 0 deletions src/rust/src/backend/kdf.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,47 @@ 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>> {
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(|_| {
// TODO: Can we trigger this? If so, we need a better error msg
// otherwise maybe we just unwrap like PBKDF2
pyo3::exceptions::PyRuntimeError::new_err(
"Error deriving key using Argon2id. This is likely due to invalid parameters.",
)
})
})?)
}

#[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

0 comments on commit be36e9b

Please sign in to comment.