Skip to content

Commit a7aa8ce

Browse files
reaperhulkalex
andauthored
argon2id support (pyca#11524)
* argon2id support * make it all rust now * set a threadpool number * address comments * set threadpool to max(available, current) * review comments * a few more improvements * Update docs/hazmat/primitives/key-derivation-functions.rst Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com> --------- Co-authored-by: Alex Gaynor <alex.gaynor@gmail.com>
1 parent 8c32661 commit a7aa8ce

File tree

9 files changed

+482
-0
lines changed

9 files changed

+482
-0
lines changed

CHANGELOG.rst

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ Changelog
2323
* Relax the Authority Key Identifier requirements on root CA certificates
2424
during X.509 verification to allow fields permitted by :rfc:`5280` but
2525
forbidden by the CA/Browser BRs.
26+
* Added support for :class:`~cryptography.hazmat.primitives.kdf.argon2.Argon2id`
27+
when using OpenSSL 3.2.0+.
2628

2729
.. _v43-0-3:
2830

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

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

34134
PBKDF2
35135
------
@@ -1039,3 +1139,4 @@ Interface
10391139
.. _`recommends`: https://datatracker.ietf.org/doc/html/rfc7914#section-2
10401140
.. _`The scrypt paper`: https://www.tarsnap.com/scrypt/scrypt.pdf
10411141
.. _`understanding HKDF`: https://soatok.blog/2021/11/17/understanding-hkdf/
1142+
.. _`parameter choice`: https://datatracker.ietf.org/doc/html/rfc9106#section-4

docs/spelling_wordlist.txt

+3
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

+6
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.Scrypt, "derive")
124124

125+
def argon2_supported(self) -> bool:
126+
if self._fips_enabled:
127+
return False
128+
else:
129+
return hasattr(rust_openssl.kdf.Argon2id, "derive")
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

+15
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,18 @@ class Scrypt:
2626
) -> None: ...
2727
def derive(self, key_material: bytes) -> bytes: ...
2828
def verify(self, key_material: bytes, expected_key: bytes) -> None: ...
29+
30+
class Argon2id:
31+
def __init__(
32+
self,
33+
*,
34+
salt: bytes,
35+
length: int,
36+
iterations: int,
37+
lanes: int,
38+
memory_cost: int,
39+
ad: bytes | None = None,
40+
secret: bytes | None = None,
41+
) -> None: ...
42+
def derive(self, key_material: bytes) -> bytes: ...
43+
def verify(self, key_material: bytes, expected_key: bytes) -> None: ...
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
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.hazmat.bindings._rust import openssl as rust_openssl
8+
from cryptography.hazmat.primitives.kdf import KeyDerivationFunction
9+
10+
Argon2id = rust_openssl.kdf.Argon2id
11+
KeyDerivationFunction.register(Argon2id)
12+
13+
__all__ = ["Argon2id"]

src/rust/src/backend/kdf.rs

+168
Original file line numberDiff line numberDiff line change
@@ -164,10 +164,178 @@ impl Scrypt {
164164
}
165165
}
166166

167+
#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")]
168+
struct Argon2id {
169+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
170+
salt: pyo3::Py<pyo3::types::PyBytes>,
171+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
172+
length: usize,
173+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
174+
iterations: u32,
175+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
176+
lanes: u32,
177+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
178+
memory_cost: u32,
179+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
180+
ad: Option<pyo3::Py<pyo3::types::PyBytes>>,
181+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
182+
secret: Option<pyo3::Py<pyo3::types::PyBytes>>,
183+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
184+
used: bool,
185+
}
186+
187+
#[pyo3::pymethods]
188+
impl Argon2id {
189+
#[new]
190+
#[pyo3(signature = (salt, length, iterations, lanes, memory_cost, ad=None, secret=None))]
191+
#[allow(clippy::too_many_arguments)]
192+
fn new(
193+
py: pyo3::Python<'_>,
194+
salt: pyo3::Py<pyo3::types::PyBytes>,
195+
length: usize,
196+
iterations: u32,
197+
lanes: u32,
198+
memory_cost: u32,
199+
ad: Option<pyo3::Py<pyo3::types::PyBytes>>,
200+
secret: Option<pyo3::Py<pyo3::types::PyBytes>>,
201+
) -> CryptographyResult<Self> {
202+
cfg_if::cfg_if! {
203+
if #[cfg(not(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER))] {
204+
_ = py;
205+
_ = salt;
206+
_ = length;
207+
_ = iterations;
208+
_ = lanes;
209+
_ = memory_cost;
210+
_ = ad;
211+
_ = secret;
212+
213+
Err(CryptographyError::from(
214+
exceptions::UnsupportedAlgorithm::new_err(
215+
"This version of OpenSSL does not support argon2id"
216+
),
217+
))
218+
} else {
219+
if cryptography_openssl::fips::is_enabled() {
220+
return Err(CryptographyError::from(
221+
exceptions::UnsupportedAlgorithm::new_err(
222+
"This version of OpenSSL does not support argon2id"
223+
),
224+
));
225+
}
226+
227+
if salt.as_bytes(py).len() < 8 {
228+
return Err(CryptographyError::from(
229+
pyo3::exceptions::PyValueError::new_err(
230+
"salt must be at least 8 bytes"
231+
),
232+
));
233+
}
234+
if length < 4 {
235+
return Err(CryptographyError::from(
236+
pyo3::exceptions::PyValueError::new_err(
237+
"length must be greater than or equal to 4."
238+
),
239+
));
240+
}
241+
if iterations < 1 {
242+
return Err(CryptographyError::from(
243+
pyo3::exceptions::PyValueError::new_err(
244+
"iterations must be greater than or equal to 1."
245+
),
246+
));
247+
}
248+
if lanes < 1 {
249+
return Err(CryptographyError::from(
250+
pyo3::exceptions::PyValueError::new_err(
251+
"lanes must be greater than or equal to 1."
252+
),
253+
));
254+
}
255+
256+
if memory_cost / 8 < lanes {
257+
return Err(CryptographyError::from(
258+
pyo3::exceptions::PyValueError::new_err(
259+
"memory_cost must be an integer >= 8 * lanes."
260+
),
261+
));
262+
}
263+
264+
265+
Ok(Argon2id{
266+
salt,
267+
length,
268+
iterations,
269+
lanes,
270+
memory_cost,
271+
ad,
272+
secret,
273+
used: false,
274+
})
275+
}
276+
}
277+
}
278+
279+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
280+
fn derive<'p>(
281+
&mut self,
282+
py: pyo3::Python<'p>,
283+
key_material: CffiBuf<'_>,
284+
) -> CryptographyResult<pyo3::Bound<'p, pyo3::types::PyBytes>> {
285+
if self.used {
286+
return Err(exceptions::already_finalized_error());
287+
}
288+
self.used = true;
289+
Ok(pyo3::types::PyBytes::new_bound_with(
290+
py,
291+
self.length,
292+
|b| {
293+
openssl::kdf::argon2id(
294+
None,
295+
key_material.as_bytes(),
296+
self.salt.as_bytes(py),
297+
self.ad.as_ref().map(|ad| ad.as_bytes(py)),
298+
self.secret.as_ref().map(|secret| secret.as_bytes(py)),
299+
self.iterations,
300+
self.lanes,
301+
self.memory_cost,
302+
b,
303+
)
304+
.map_err(CryptographyError::from)?;
305+
Ok(())
306+
},
307+
)?)
308+
}
309+
310+
#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)]
311+
fn verify(
312+
&mut self,
313+
py: pyo3::Python<'_>,
314+
key_material: CffiBuf<'_>,
315+
expected_key: CffiBuf<'_>,
316+
) -> CryptographyResult<()> {
317+
let actual = self.derive(py, key_material)?;
318+
let actual_bytes = actual.as_bytes();
319+
let expected_bytes = expected_key.as_bytes();
320+
321+
if actual_bytes.len() != expected_bytes.len()
322+
|| !openssl::memcmp::eq(actual_bytes, expected_bytes)
323+
{
324+
return Err(CryptographyError::from(exceptions::InvalidKey::new_err(
325+
"Keys do not match.",
326+
)));
327+
}
328+
329+
Ok(())
330+
}
331+
}
332+
167333
#[pyo3::pymodule]
168334
pub(crate) mod kdf {
169335
#[pymodule_export]
170336
use super::derive_pbkdf2_hmac;
171337
#[pymodule_export]
338+
use super::Argon2id;
339+
#[pymodule_export]
172340
use super::Scrypt;
173341
}

src/rust/src/lib.rs

+14
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,20 @@ mod _rust {
225225
openssl_mod.add("_legacy_provider_loaded", false)?;
226226
}
227227
}
228+
cfg_if::cfg_if! {
229+
if #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] {
230+
use std::ptr;
231+
use std::cmp::max;
232+
233+
let available = std::thread::available_parallelism().map_or(0, |v| v.get() as u64);
234+
// SAFETY: This sets a libctx provider limit, but we always use the same libctx by passing NULL.
235+
unsafe {
236+
let current = openssl_sys::OSSL_get_max_threads(ptr::null_mut());
237+
// Set the thread limit to the max of available parallelism or current limit.
238+
openssl_sys::OSSL_set_max_threads(ptr::null_mut(), max(available, current));
239+
}
240+
}
241+
}
228242

229243
Ok(())
230244
}

0 commit comments

Comments
 (0)