Skip to content

Commit e37c3df

Browse files
committed
argon2id support
1 parent 408b9f8 commit e37c3df

File tree

13 files changed

+416
-13
lines changed

13 files changed

+416
-13
lines changed

CHANGELOG.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Changelog
1515
* Relax the Authority Key Identifier requirements on root CA certificates
1616
during X.509 verification to allow fields permitted by :rfc:`5280` but
1717
forbidden by the CA/Browser BRs.
18+
* Added support for :class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`.
1819

1920
.. _v43-0-0:
2021

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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,105 @@ Different KDFs are suitable for different tasks such as:
3030
Variable cost algorithms
3131
~~~~~~~~~~~~~~~~~~~~~~~~
3232

33+
Argon2id
34+
--------
35+
36+
.. currentmodule:: cryptography.hazmat.primitives.kdf.argon2
37+
38+
.. class:: Argon2id(*, salt, length, iterations, lanes, memory_cost, ad=None, secret=None)
39+
40+
.. versionadded:: 44.0.0
41+
42+
Argon2id is a KDF designed for password storage. It is designed to be
43+
resistant to hardware attacks and is described in :rfc:`9106`.
44+
45+
This class conforms to the
46+
:class:`~cryptography.hazmat.primitives.kdf.KeyDerivationFunction`
47+
interface.
48+
49+
.. doctest::
50+
51+
>>> import os
52+
>>> from cryptography.hazmat.primitives.kdf.argon2 import Argon2id
53+
>>> salt = os.urandom(16)
54+
>>> # derive
55+
>>> kdf = Argon2id(
56+
... salt=salt,
57+
... length=32,
58+
... iterations=1,
59+
... lanes=4,
60+
... memory_cost=64 * 1024,
61+
... ad=None,
62+
... secret=None,
63+
... )
64+
>>> key = kdf.derive(b"my great password")
65+
>>> # verify
66+
>>> kdf = Argon2id(
67+
... salt=salt,
68+
... length=32,
69+
... iterations=1,
70+
... lanes=4,
71+
... memory_cost=64 * 1024,
72+
... ad=None,
73+
... secret=None,
74+
... )
75+
>>> kdf.verify(b"my great password", key)
76+
77+
**All arguments to the constructor are keyword-only.**
78+
79+
:param bytes salt: A salt.
80+
:param int length: The desired length of the derived key in bytes.
81+
:param int iterations: Also known as passes, this is used to tune
82+
the running time independently of the memory size.
83+
:param int lanes: The number of lanes (parallel threads) to use. Also
84+
known as parallelism.
85+
:param int memory_cost: The amount of memory to use in kibibytes.
86+
1 kibibyte (KiB) is 1024 bytes.
87+
:param bytes ad: Optional associated data.
88+
:param bytes secret: Optional secret data.
89+
90+
:rfc:`9106` has recommendations for `parameter choice`_.
91+
92+
:raises cryptography.exceptions.UnsupportedAlgorithm: If Argon2id is not
93+
supported by the OpenSSL version ``cryptography`` is using.
94+
95+
.. method:: derive(key_material)
96+
97+
:param key_material: The input key material.
98+
:type key_material: :term:`bytes-like`
99+
:return bytes: the derived key.
100+
:raises TypeError: This exception is raised if ``key_material`` is not
101+
``bytes``.
102+
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
103+
:meth:`derive` or
104+
:meth:`verify` is
105+
called more than
106+
once.
107+
108+
This generates and returns a new key from the supplied password.
109+
110+
.. method:: verify(key_material, expected_key)
111+
112+
:param bytes key_material: The input key material. This is the same as
113+
``key_material`` in :meth:`derive`.
114+
:param bytes expected_key: The expected result of deriving a new key,
115+
this is the same as the return value of
116+
:meth:`derive`.
117+
:raises cryptography.exceptions.InvalidKey: This is raised when the
118+
derived key does not match
119+
the expected key.
120+
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
121+
:meth:`derive` or
122+
:meth:`verify` is
123+
called more than
124+
once.
125+
126+
This checks whether deriving a new key from the supplied
127+
``key_material`` generates the same key as the ``expected_key``, and
128+
raises an exception if they do not match. This can be used for
129+
checking whether the password a user provides matches the stored derived
130+
key.
131+
33132

34133
PBKDF2
35134
------
@@ -1039,3 +1138,4 @@ Interface
10391138
.. _`recommends`: https://datatracker.ietf.org/doc/html/rfc7914#section-2
10401139
.. _`The scrypt paper`: https://www.tarsnap.com/scrypt/scrypt.pdf
10411140
.. _`understanding HKDF`: https://soatok.blog/2021/11/17/understanding-hkdf/
1141+
.. _`parameter choice`: https://datatracker.ietf.org/doc/html/rfc9106#section-4

docs/spelling_wordlist.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ iOS
7777
iterable
7878
Kerberos
7979
Keychain
80+
KiB
81+
kibibyte
82+
kibibytes
8083
Koblitz
8184
Lange
8285
logins

src/cryptography/hazmat/backends/openssl/backend.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,12 @@ def scrypt_supported(self) -> bool:
122122
else:
123123
return hasattr(rust_openssl.kdf, "derive_scrypt")
124124

125+
def argon2_supported(self) -> bool:
126+
if self._fips_enabled:
127+
return False
128+
else:
129+
return hasattr(rust_openssl.kdf, "derive_argon2id")
130+
125131
def hmac_supported(self, algorithm: hashes.HashAlgorithm) -> bool:
126132
# FIPS mode still allows SHA1 for HMAC
127133
if self._fips_enabled and isinstance(algorithm, hashes.SHA1):

src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ def derive_scrypt(
2020
max_mem: int,
2121
length: int,
2222
) -> bytes: ...
23+
def derive_argon2id(
24+
key_material: bytes,
25+
salt: bytes,
26+
length: int,
27+
iterations: int,
28+
lanes: int,
29+
memory_cost: int,
30+
ad: bytes | None,
31+
secret: bytes | None,
32+
) -> bytes: ...
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# This file is dual licensed under the terms of the Apache License, Version
2+
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
3+
# for complete details.
4+
5+
from __future__ import annotations
6+
7+
from cryptography import utils
8+
from cryptography.exceptions import (
9+
AlreadyFinalized,
10+
InvalidKey,
11+
UnsupportedAlgorithm,
12+
)
13+
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
14+
from cryptography.hazmat.primitives import constant_time
15+
from cryptography.hazmat.primitives.kdf import KeyDerivationFunction
16+
17+
18+
class Argon2id(KeyDerivationFunction):
19+
def __init__(
20+
self,
21+
*,
22+
salt: bytes,
23+
length: int,
24+
iterations: int,
25+
lanes: int,
26+
memory_cost: int,
27+
ad: bytes | None = None,
28+
secret: bytes | None = None,
29+
):
30+
from cryptography.hazmat.backends.openssl.backend import (
31+
backend as ossl,
32+
)
33+
34+
if not ossl.argon2_supported():
35+
raise UnsupportedAlgorithm(
36+
"This version of OpenSSL does not support argon2id"
37+
)
38+
39+
utils._check_bytes("salt", salt)
40+
# OpenSSL requires a salt of at least 8 bytes
41+
if len(salt) < 8:
42+
raise ValueError("salt must be at least 8 bytes")
43+
# Minimum length is 4 bytes as specified in RFC 9106
44+
if not isinstance(length, int) or length < 4:
45+
raise ValueError("length must be an integer greater >= 4")
46+
if not isinstance(iterations, int) or iterations < 1:
47+
raise ValueError("iterations must be an integer greater than 0")
48+
if not isinstance(lanes, int) or lanes < 1:
49+
raise ValueError("lanes must be an integer greater than 0")
50+
# Memory cost must be at least 8 * lanes
51+
if not isinstance(memory_cost, int) or memory_cost < 8 * lanes:
52+
raise ValueError("memory_cost must be an integer >= 8 * lanes")
53+
if ad is not None:
54+
utils._check_bytes("ad", ad)
55+
if secret is not None:
56+
utils._check_bytes("secret", secret)
57+
58+
self._used = False
59+
self._salt = salt
60+
self._length = length
61+
self._iterations = iterations
62+
self._lanes = lanes
63+
self._memory_cost = memory_cost
64+
self._ad = ad
65+
self._secret = secret
66+
67+
def derive(self, key_material: bytes) -> bytes:
68+
if self._used:
69+
raise AlreadyFinalized("argon2id instances can only be used once.")
70+
self._used = True
71+
72+
utils._check_byteslike("key_material", key_material)
73+
74+
return rust_openssl.kdf.derive_argon2id(
75+
key_material,
76+
self._salt,
77+
self._length,
78+
self._iterations,
79+
self._lanes,
80+
self._memory_cost,
81+
self._ad,
82+
self._secret,
83+
)
84+
85+
def verify(self, key_material: bytes, expected_key: bytes) -> None:
86+
if not constant_time.bytes_eq(self.derive(key_material), expected_key):
87+
raise InvalidKey

src/rust/Cargo.lock

Lines changed: 3 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/rust/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ cryptography-x509 = { path = "cryptography-x509" }
2626
cryptography-x509-verification = { path = "cryptography-x509-verification" }
2727
cryptography-openssl = { path = "cryptography-openssl" }
2828
pem = { version = "3", default-features = false }
29-
openssl = "0.10.66"
30-
openssl-sys = "0.9.103"
29+
openssl = { git = "https://github.com/sfackler/rust-openssl" }
30+
openssl-sys = { git = "https://github.com/sfackler/rust-openssl" }
3131
foreign-types-shared = "0.1"
3232
self_cell = "1"
3333

src/rust/cryptography-cffi/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ rust-version.workspace = true
88

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

1313
[build-dependencies]
1414
cc = "1.1.15"

src/rust/cryptography-key-parsing/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,6 @@ rust-version.workspace = true
99
[dependencies]
1010
asn1 = { version = "0.17.0", default-features = false }
1111
cfg-if = "1"
12-
openssl = "0.10.66"
13-
openssl-sys = "0.9.103"
12+
openssl = { git = "https://github.com/sfackler/rust-openssl" }
13+
openssl-sys = { git = "https://github.com/sfackler/rust-openssl" }
1414
cryptography-x509 = { path = "../cryptography-x509" }

0 commit comments

Comments
 (0)