Skip to content

Commit be36e9b

Browse files
committed
argon2id support
1 parent 408b9f8 commit be36e9b

File tree

13 files changed

+399
-13
lines changed

13 files changed

+399
-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: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,103 @@ 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+
:param bytes salt: A salt.
78+
:param int length: The desired length of the derived key in bytes.
79+
:param int iterations: Also known as passes, this is used to tune
80+
the running time independently of the memory size.
81+
:param int lanes: The number of lanes (parallel threads) to use. Also
82+
known as parallelism.
83+
:param int memory_cost: The amount of memory to use in kibibytes.
84+
1 kibibyte (KiB) is 1024 bytes.
85+
:param bytes ad: Optional associated data.
86+
:param bytes secret: Optional secret data.
87+
88+
:rfc:`9106` has recommendations for `parameter choice`_.
89+
90+
:raises cryptography.exceptions.UnsupportedAlgorithm: If Argon2id is not
91+
supported by the OpenSSL version ``cryptography`` is using.
92+
93+
.. method:: derive(key_material)
94+
95+
:param key_material: The input key material.
96+
:type key_material: :term:`bytes-like`
97+
:return bytes: the derived key.
98+
:raises TypeError: This exception is raised if ``key_material`` is not
99+
``bytes``.
100+
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
101+
:meth:`derive` or
102+
:meth:`verify` is
103+
called more than
104+
once.
105+
106+
This generates and returns a new key from the supplied password.
107+
108+
.. method:: verify(key_material, expected_key)
109+
110+
:param bytes key_material: The input key material. This is the same as
111+
``key_material`` in :meth:`derive`.
112+
:param bytes expected_key: The expected result of deriving a new key,
113+
this is the same as the return value of
114+
:meth:`derive`.
115+
:raises cryptography.exceptions.InvalidKey: This is raised when the
116+
derived key does not match
117+
the expected key.
118+
:raises cryptography.exceptions.AlreadyFinalized: This is raised when
119+
:meth:`derive` or
120+
:meth:`verify` is
121+
called more than
122+
once.
123+
124+
This checks whether deriving a new key from the supplied
125+
``key_material`` generates the same key as the ``expected_key``, and
126+
raises an exception if they do not match. This can be used for
127+
checking whether the password a user provides matches the stored derived
128+
key.
129+
33130

34131
PBKDF2
35132
------
@@ -1039,3 +1136,4 @@ Interface
10391136
.. _`recommends`: https://datatracker.ietf.org/doc/html/rfc7914#section-2
10401137
.. _`The scrypt paper`: https://www.tarsnap.com/scrypt/scrypt.pdf
10411138
.. _`understanding HKDF`: https://soatok.blog/2021/11/17/understanding-hkdf/
1139+
.. _`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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
salt: bytes,
22+
length: int,
23+
iterations: int,
24+
lanes: int,
25+
memory_cost: int,
26+
ad: bytes | None = None,
27+
secret: bytes | None = None,
28+
):
29+
from cryptography.hazmat.backends.openssl.backend import (
30+
backend as ossl,
31+
)
32+
33+
if ossl.openssl_version_number() < 0x30200000:
34+
raise UnsupportedAlgorithm(
35+
"This version of OpenSSL does not support argon2id"
36+
)
37+
38+
utils._check_bytes("salt", salt)
39+
# OpenSSL requires a salt of at least 8 bytes
40+
if len(salt) < 8:
41+
raise ValueError("salt must be at least 8 bytes")
42+
# Minimum length is 4 bytes as specified in RFC 9106
43+
if not isinstance(length, int) or length < 4:
44+
raise ValueError("length must be an integer greater >= 4")
45+
if not isinstance(iterations, int) or iterations < 1:
46+
raise ValueError("iterations must be an integer greater than 0")
47+
if not isinstance(lanes, int) or lanes < 1:
48+
raise ValueError("lanes must be an integer greater than 0")
49+
# Memory cost must be at least 8 * lanes
50+
if not isinstance(memory_cost, int) or memory_cost < 8 * lanes:
51+
raise ValueError("memory_cost must be an integer >= 8 * lanes")
52+
if ad is not None:
53+
utils._check_bytes("ad", ad)
54+
if secret is not None:
55+
utils._check_bytes("secret", secret)
56+
57+
self._used = False
58+
self._salt = salt
59+
self._length = length
60+
self._iterations = iterations
61+
self._lanes = lanes
62+
self._memory_cost = memory_cost
63+
self._ad = ad
64+
self._secret = secret
65+
66+
def derive(self, key_material: bytes) -> bytes:
67+
if self._used:
68+
raise AlreadyFinalized("argon2id instances can only be used once.")
69+
self._used = True
70+
71+
utils._check_byteslike("key_material", key_material)
72+
73+
return rust_openssl.kdf.derive_argon2id(
74+
key_material,
75+
self._salt,
76+
self._length,
77+
self._iterations,
78+
self._lanes,
79+
self._memory_cost,
80+
self._ad,
81+
self._secret,
82+
)
83+
84+
def verify(self, key_material: bytes, expected_key: bytes) -> None:
85+
if not constant_time.bytes_eq(self.derive(key_material), expected_key):
86+
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)