From 22e8fd77858ff9293a83c3273500fd88971e995a Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 20 Oct 2025 12:17:46 -0400 Subject: [PATCH 01/12] Adding test vectors for argon2d and argon2i variants --- vectors/cryptography_vectors/KDF/argon2d.txt | 44 ++++++++++++++++ vectors/cryptography_vectors/KDF/argon2i.txt | 53 ++++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 vectors/cryptography_vectors/KDF/argon2d.txt create mode 100644 vectors/cryptography_vectors/KDF/argon2i.txt diff --git a/vectors/cryptography_vectors/KDF/argon2d.txt b/vectors/cryptography_vectors/KDF/argon2d.txt new file mode 100644 index 000000000000..20decde83e5a --- /dev/null +++ b/vectors/cryptography_vectors/KDF/argon2d.txt @@ -0,0 +1,44 @@ +# Test vectors from RFC 9106, +# https://github.com/openssl/openssl/blob/01f4b44e075a796d62d3b007a80c5c04d0e77bfb/test/recipes/30-test_evp_data/evpkdf_argon2.txt +# Adapted for the pyca/cryptography NIST loaders + +COUNT = 0 +length = 32 +lanes = 4 +iter = 3 +memcost = 32 +secret = 0303030303030303 +pass = 0101010101010101010101010101010101010101010101010101010101010101 +salt = 02020202020202020202020202020202 +ad = 040404040404040404040404 +output = 512B391B6F1162975371D30919734294F868E3BE3984F3C1A13A4DB9FABE4ACB + +# echo -n "password" | argon2 pycasalt -d -t 1 -k 131072 -p 2 -l 64 +COUNT = 1 +length = 64 +lanes = 2 +iter = 1 +memcost = 131072 +salt = 7079636173616c74 +pass = 70617373776f7264 +output = 8868f5fe0f9c138ef691cd74243cbc7eec42ad4e5a69416cbb744c0d6b751971e22107143986dbb747899451a5f99d4a8eeb102d5852847f473c22e55a040fe7 + +# echo -n "password" | argon2 pycasalt -d -t 4 -k 50 -p 4 -l 8 +COUNT = 2 +length = 8 +lanes = 4 +iter = 4 +memcost = 50 +salt = 7079636173616c74 +pass = 70617373776f7264 +output = 29fec2f9cecd8716 + +# echo -n "password" | argon2 pycasalt -d -t 1 -k 8 -p 1 -l 4 +COUNT = 3 +length = 4 +lanes = 1 +iter = 1 +memcost = 8 +salt = 7079636173616c74 +pass = 70617373776f7264 +output = a60137a5 \ No newline at end of file diff --git a/vectors/cryptography_vectors/KDF/argon2i.txt b/vectors/cryptography_vectors/KDF/argon2i.txt new file mode 100644 index 000000000000..186b2bf3241a --- /dev/null +++ b/vectors/cryptography_vectors/KDF/argon2i.txt @@ -0,0 +1,53 @@ +# Test vectors from RFC 9106, +# https://github.com/openssl/openssl/blob/01f4b44e075a796d62d3b007a80c5c04d0e77bfb/test/recipes/30-test_evp_data/evpkdf_argon2.txt +# and the argon2 CLI tool. Adapted for the pyca/cryptography NIST loaders + +COUNT = 0 +length = 32 +lanes = 4 +iter = 3 +memcost = 32 +secret = 0303030303030303 +pass = 0101010101010101010101010101010101010101010101010101010101010101 +salt = 02020202020202020202020202020202 +ad = 040404040404040404040404 +output = C814D9D1DC7F37AA13F0D77F2494BDA1C8DE6B016DD388D29952A4C4672B6CE8 + +COUNT = 1 +length = 32 +lanes = 4 +iter = 3 +memcost = 32 +pass = 0101010101010101010101010101010101010101010101010101010101010101 +salt = 02020202020202020202020202020202 +output = A9A7510E6DB4D588BA3414CD0E094D480D683F97B9CCB612A544FE8EF65BA8E0 + +# echo -n "password" | argon2 pycasalt -i -t 1 -k 131072 -p 2 -l 64 +COUNT = 2 +length = 64 +lanes = 2 +iter = 1 +memcost = 131072 +salt = 7079636173616c74 +pass = 70617373776f7264 +output = 677c326cbd6d3697a1a1433750418795c20414048ff473fa53c0f594b8998f8b58fe36f0eb7b88ee8d9ff5246b9457f02d96dd38c8e062063faf795f877977c5 + +# echo -n "password" | argon2 pycasalt -i -t 4 -k 50 -p 4 -l 8 +COUNT = 3 +length = 8 +lanes = 4 +iter = 4 +memcost = 50 +salt = 7079636173616c74 +pass = 70617373776f7264 +output = 87a30dd30bb00555 + +# echo -n "password" | argon2 pycasalt -i -t 1 -k 8 -p 1 -l 4 +COUNT = 4 +length = 4 +lanes = 1 +iter = 1 +memcost = 8 +salt = 7079636173616c74 +pass = 70617373776f7264 +output = 040180d6 \ No newline at end of file From ab5adc8ebba62786bd32691728de894ada5c0582 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 20 Oct 2025 13:38:51 -0400 Subject: [PATCH 02/12] Initial support backend for Argon2d and Argon2i variants - Adding python class stubs --- .../hazmat/bindings/_rust/openssl/kdf.pyi | 40 +++ .../hazmat/primitives/kdf/argon2.py | 6 +- src/rust/src/backend/kdf.rs | 323 ++++++++++++++++-- 3 files changed, 347 insertions(+), 22 deletions(-) diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi index 6af23c15fe78..83fded449e2f 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi @@ -32,6 +32,46 @@ class Scrypt: def derive(self, key_material: Buffer) -> bytes: ... def verify(self, key_material: bytes, expected_key: bytes) -> None: ... +class Argon2d: + def __init__( + self, + *, + salt: bytes, + length: int, + iterations: int, + lanes: int, + memory_cost: int, + ad: bytes | None = None, + secret: bytes | None = None, + ) -> None: ... + def derive(self, key_material: bytes) -> bytes: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + def derive_phc_encoded(self, key_material: bytes) -> str: ... + @classmethod + def verify_phc_encoded( + cls, key_material: bytes, phc_encoded: str, secret: bytes | None = None + ) -> None: ... + +class Argon2i: + def __init__( + self, + *, + salt: bytes, + length: int, + iterations: int, + lanes: int, + memory_cost: int, + ad: bytes | None = None, + secret: bytes | None = None, + ) -> None: ... + def derive(self, key_material: bytes) -> bytes: ... + def verify(self, key_material: bytes, expected_key: bytes) -> None: ... + def derive_phc_encoded(self, key_material: bytes) -> str: ... + @classmethod + def verify_phc_encoded( + cls, key_material: bytes, phc_encoded: str, secret: bytes | None = None + ) -> None: ... + class Argon2id: def __init__( self, diff --git a/src/cryptography/hazmat/primitives/kdf/argon2.py b/src/cryptography/hazmat/primitives/kdf/argon2.py index 405fc8dff268..03e84d48ad41 100644 --- a/src/cryptography/hazmat/primitives/kdf/argon2.py +++ b/src/cryptography/hazmat/primitives/kdf/argon2.py @@ -7,7 +7,11 @@ from cryptography.hazmat.bindings._rust import openssl as rust_openssl from cryptography.hazmat.primitives.kdf import KeyDerivationFunction +Argon2d = rust_openssl.kdf.Argon2d +Argon2i = rust_openssl.kdf.Argon2i Argon2id = rust_openssl.kdf.Argon2id +KeyDerivationFunction.register(Argon2d) +KeyDerivationFunction.register(Argon2i) KeyDerivationFunction.register(Argon2id) -__all__ = ["Argon2id"] +__all__ = ["Argon2d", "Argon2i", "Argon2id"] diff --git a/src/rust/src/backend/kdf.rs b/src/rust/src/backend/kdf.rs index f4baaad28502..03b7c17954c6 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -238,8 +238,13 @@ impl Scrypt { } } -#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] -struct Argon2id { +enum Argon2Variant { + Argon2d, + Argon2i, + Argon2id, +} + +struct BaseArgon2 { #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] salt: pyo3::Py, #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] @@ -258,10 +263,7 @@ struct Argon2id { used: bool, } -#[pyo3::pymethods] -impl Argon2id { - #[new] - #[pyo3(signature = (salt, length, iterations, lanes, memory_cost, ad=None, secret=None))] +impl BaseArgon2 { #[allow(clippy::too_many_arguments)] fn new( py: pyo3::Python<'_>, @@ -286,14 +288,14 @@ impl Argon2id { Err(CryptographyError::from( exceptions::UnsupportedAlgorithm::new_err( - "This version of OpenSSL does not support argon2id" + "This version of OpenSSL does not support argon2" ), )) } else { if cryptography_openssl::fips::is_enabled() { return Err(CryptographyError::from( exceptions::UnsupportedAlgorithm::new_err( - "This version of OpenSSL does not support argon2id" + "This version of OpenSSL does not support argon2" ), )); } @@ -335,8 +337,7 @@ impl Argon2id { )); } - - Ok(Argon2id{ + Ok(Self{ salt, length, iterations, @@ -354,14 +355,22 @@ impl Argon2id { fn derive<'p>( &mut self, py: pyo3::Python<'p>, + variant: &Argon2Variant, key_material: CffiBuf<'_>, ) -> CryptographyResult> { if self.used { return Err(exceptions::already_finalized_error()); } self.used = true; + + let derive_fn = match &variant { + Argon2Variant::Argon2d => openssl::kdf::argon2d, + Argon2Variant::Argon2i => openssl::kdf::argon2i, + Argon2Variant::Argon2id => openssl::kdf::argon2id, + }; + Ok(pyo3::types::PyBytes::new_with(py, self.length, |b| { - openssl::kdf::argon2id( + (derive_fn)( None, key_material.as_bytes(), self.salt.as_bytes(py), @@ -381,10 +390,11 @@ impl Argon2id { fn verify( &mut self, py: pyo3::Python<'_>, + variant: &Argon2Variant, key_material: CffiBuf<'_>, expected_key: CffiBuf<'_>, ) -> CryptographyResult<()> { - let actual = self.derive(py, key_material)?; + let actual = self.derive(py, variant, key_material)?; let actual_bytes = actual.as_bytes(); let expected_bytes = expected_key.as_bytes(); @@ -401,35 +411,47 @@ impl Argon2id { fn derive_phc_encoded<'p>( &mut self, py: pyo3::Python<'p>, + variant: &Argon2Variant, key_material: CffiBuf<'_>, ) -> CryptographyResult> { - let derived_key = self.derive(py, key_material)?; + let derived_key = self.derive(py, variant, key_material)?; let salt_bytes = self.salt.as_bytes(py); let salt_b64 = STANDARD_NO_PAD.encode(salt_bytes); let hash_b64 = STANDARD_NO_PAD.encode(derived_key.as_bytes()); + let variant_id: &str = match variant { + Argon2Variant::Argon2d => "argon2d", + Argon2Variant::Argon2i => "argon2i", + Argon2Variant::Argon2id => "argon2id", + }; + // Format the PHC string let phc_string = format!( - "$argon2id$v=19$m={},t={},p={}${}${}", - self.memory_cost, self.iterations, self.lanes, salt_b64, hash_b64 + "${}$v=19$m={},t={},p={}${}${}", + variant_id, self.memory_cost, self.iterations, self.lanes, salt_b64, hash_b64 ); Ok(pyo3::types::PyString::new(py, &phc_string)) } #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] - #[staticmethod] - #[pyo3(signature = (key_material, phc_encoded, secret=None))] fn verify_phc_encoded( py: pyo3::Python<'_>, + variant: &Argon2Variant, key_material: CffiBuf<'_>, phc_encoded: &str, secret: Option>, ) -> CryptographyResult<()> { let parts: Vec<_> = phc_encoded.split('$').collect(); - if parts.len() != 6 || !parts[0].is_empty() || parts[1] != "argon2id" { + let variant_id: &str = match variant { + Argon2Variant::Argon2d => "argon2d", + Argon2Variant::Argon2i => "argon2i", + Argon2Variant::Argon2id => "argon2id", + }; + + if parts.len() != 6 || !parts[0].is_empty() || parts[1] != variant_id { return Err(CryptographyError::from(exceptions::InvalidKey::new_err( "Invalid PHC string format.", ))); @@ -493,7 +515,7 @@ impl Argon2id { })?; let salt = pyo3::types::PyBytes::new(py, &salt_bytes); - let mut argon2 = Argon2id::new( + let mut argon2 = BaseArgon2::new( py, salt.into(), hash_bytes.len(), @@ -504,7 +526,7 @@ impl Argon2id { secret, )?; - let derived_key = argon2.derive(py, key_material)?; + let derived_key = argon2.derive(py, variant, key_material)?; let derived_bytes = derived_key.as_bytes(); if !constant_time::bytes_eq(derived_bytes, &hash_bytes) { @@ -517,6 +539,265 @@ impl Argon2id { } } +#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] +struct Argon2d { + #[allow(unused)] + base: BaseArgon2, +} + +#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] +struct Argon2i { + #[allow(unused)] + base: BaseArgon2, +} + +#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] +struct Argon2id { + #[allow(unused)] + base: BaseArgon2, +} + +#[pyo3::pymethods] +impl Argon2d { + #[new] + #[pyo3(signature = (salt, length, iterations, lanes, memory_cost, ad=None, secret=None))] + #[allow(clippy::too_many_arguments)] + fn new( + py: pyo3::Python<'_>, + salt: pyo3::Py, + length: usize, + iterations: u32, + lanes: u32, + memory_cost: u32, + ad: Option>, + secret: Option>, + ) -> CryptographyResult { + Ok({ + Self { + base: BaseArgon2::new( + py, + salt, + length, + iterations, + lanes, + memory_cost, + ad, + secret, + )?, + } + }) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn derive<'p>( + &mut self, + py: pyo3::Python<'p>, + key_material: CffiBuf<'_>, + ) -> CryptographyResult> { + self.base.derive(py, &Argon2Variant::Argon2d, key_material) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn verify( + &mut self, + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + expected_key: CffiBuf<'_>, + ) -> CryptographyResult<()> { + self.base + .verify(py, &Argon2Variant::Argon2d, key_material, expected_key) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn derive_phc_encoded<'p>( + &mut self, + py: pyo3::Python<'p>, + key_material: CffiBuf<'_>, + ) -> CryptographyResult> { + self.base + .derive_phc_encoded(py, &Argon2Variant::Argon2d, key_material) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + #[staticmethod] + #[pyo3(signature = (key_material, phc_encoded, secret=None))] + fn verify_phc_encoded( + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + phc_encoded: &str, + secret: Option>, + ) -> CryptographyResult<()> { + BaseArgon2::verify_phc_encoded( + py, + &Argon2Variant::Argon2d, + key_material, + phc_encoded, + secret, + ) + } +} + +#[pyo3::pymethods] +impl Argon2i { + #[new] + #[pyo3(signature = (salt, length, iterations, lanes, memory_cost, ad=None, secret=None))] + #[allow(clippy::too_many_arguments)] + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn new( + py: pyo3::Python<'_>, + salt: pyo3::Py, + length: usize, + iterations: u32, + lanes: u32, + memory_cost: u32, + ad: Option>, + secret: Option>, + ) -> CryptographyResult { + Ok({ + Self { + base: BaseArgon2::new( + py, + salt, + length, + iterations, + lanes, + memory_cost, + ad, + secret, + )?, + } + }) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn derive<'p>( + &mut self, + py: pyo3::Python<'p>, + key_material: CffiBuf<'_>, + ) -> CryptographyResult> { + self.base.derive(py, &Argon2Variant::Argon2i, key_material) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn verify( + &mut self, + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + expected_key: CffiBuf<'_>, + ) -> CryptographyResult<()> { + self.base + .verify(py, &Argon2Variant::Argon2i, key_material, expected_key) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn derive_phc_encoded<'p>( + &mut self, + py: pyo3::Python<'p>, + key_material: CffiBuf<'_>, + ) -> CryptographyResult> { + self.base + .derive_phc_encoded(py, &Argon2Variant::Argon2i, key_material) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + #[staticmethod] + #[pyo3(signature = (key_material, phc_encoded, secret=None))] + fn verify_phc_encoded( + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + phc_encoded: &str, + secret: Option>, + ) -> CryptographyResult<()> { + BaseArgon2::verify_phc_encoded( + py, + &Argon2Variant::Argon2i, + key_material, + phc_encoded, + secret, + ) + } +} + +#[pyo3::pymethods] +impl Argon2id { + #[new] + #[pyo3(signature = (salt, length, iterations, lanes, memory_cost, ad=None, secret=None))] + #[allow(clippy::too_many_arguments)] + fn new( + py: pyo3::Python<'_>, + salt: pyo3::Py, + length: usize, + iterations: u32, + lanes: u32, + memory_cost: u32, + ad: Option>, + secret: Option>, + ) -> CryptographyResult { + Ok({ + Self { + base: BaseArgon2::new( + py, + salt, + length, + iterations, + lanes, + memory_cost, + ad, + secret, + )?, + } + }) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn derive<'p>( + &mut self, + py: pyo3::Python<'p>, + key_material: CffiBuf<'_>, + ) -> CryptographyResult> { + self.base.derive(py, &Argon2Variant::Argon2id, key_material) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn verify( + &mut self, + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + expected_key: CffiBuf<'_>, + ) -> CryptographyResult<()> { + self.base + .verify(py, &Argon2Variant::Argon2id, key_material, expected_key) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + fn derive_phc_encoded<'p>( + &mut self, + py: pyo3::Python<'p>, + key_material: CffiBuf<'_>, + ) -> CryptographyResult> { + self.base + .derive_phc_encoded(py, &Argon2Variant::Argon2id, key_material) + } + + #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] + #[staticmethod] + #[pyo3(signature = (key_material, phc_encoded, secret=None))] + fn verify_phc_encoded( + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + phc_encoded: &str, + secret: Option>, + ) -> CryptographyResult<()> { + BaseArgon2::verify_phc_encoded( + py, + &Argon2Variant::Argon2id, + key_material, + phc_encoded, + secret, + ) + } +} + #[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.hkdf", name = "HKDF")] struct Hkdf { algorithm: pyo3::Py, @@ -837,5 +1118,5 @@ impl HkdfExpand { #[pyo3::pymodule(gil_used = false)] pub(crate) mod kdf { #[pymodule_export] - use super::{Argon2id, Hkdf, HkdfExpand, Pbkdf2Hmac, Scrypt}; + use super::{Argon2d, Argon2i, Argon2id, Hkdf, HkdfExpand, Pbkdf2Hmac, Scrypt}; } From 23f862b25283726695f6430c7e4711caa1147c21 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 20 Oct 2025 13:40:35 -0400 Subject: [PATCH 03/12] Updating pytests to cover Argon2d and Argon2i variants --- tests/hazmat/primitives/test_argon2.py | 111 +++++++++++++++---------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/tests/hazmat/primitives/test_argon2.py b/tests/hazmat/primitives/test_argon2.py index 8db9c8cf0c92..bdb7e2dd58f7 100644 --- a/tests/hazmat/primitives/test_argon2.py +++ b/tests/hazmat/primitives/test_argon2.py @@ -10,16 +10,28 @@ import pytest from cryptography.exceptions import AlreadyFinalized, InvalidKey -from cryptography.hazmat.primitives.kdf.argon2 import Argon2id +from cryptography.hazmat.primitives.kdf.argon2 import ( + Argon2d, + Argon2i, + Argon2id, +) from tests.utils import ( load_nist_vectors, load_vectors_from_file, raises_unsupported_algorithm, ) -vectors = load_vectors_from_file( - os.path.join("KDF", "argon2id.txt"), load_nist_vectors -) +variants = (Argon2d, Argon2i, Argon2id) + +vectors: list[tuple[type, dict]] = [] +for clazz in variants: + vectors.extend( + (clazz, x) + for x in load_vectors_from_file( + os.path.join("KDF", f"{clazz.__name__.lower()}.txt"), + load_nist_vectors, + ) + ) @pytest.mark.supported( @@ -37,9 +49,20 @@ def test_unsupported_backend(backend): only_if=lambda backend: backend.argon2_supported(), skip_message="Argon2id not supported by this version of OpenSSL", ) -class TestArgon2id: - @pytest.mark.parametrize("params", vectors) +class TestArgon2: + VECTORS = load_vectors_from_file( + os.path.join("KDF", "argon2id.txt"), load_nist_vectors + ) + + @pytest.fixture(scope="class", params=variants) + def clazz(self, request) -> type: + return request.param + + @pytest.mark.parametrize( + "params", vectors, ids=lambda x: f"{x[0].__name__}-params" + ) def test_derive(self, params, backend): + argon_clazz, params = params salt = binascii.unhexlify(params["salt"]) ad = binascii.unhexlify(params["ad"]) if "ad" in params else None secret = ( @@ -54,7 +77,7 @@ def test_derive(self, params, backend): password = binascii.unhexlify(params["pass"]) derived_key = params["output"].lower() - argon2id = Argon2id( + variant = argon_clazz( salt=salt, length=length, iterations=iterations, @@ -63,12 +86,12 @@ def test_derive(self, params, backend): ad=ad, secret=secret, ) - assert binascii.hexlify(argon2id.derive(password)) == derived_key + assert binascii.hexlify(variant.derive(password)) == derived_key - def test_invalid_types(self, backend): + def test_invalid_types(self, clazz, backend): with pytest.raises(TypeError): - Argon2id( - salt="notbytes", # type: ignore[arg-type] + clazz( + salt="notbytes", length=32, iterations=1, lanes=1, @@ -78,25 +101,25 @@ def test_invalid_types(self, backend): ) with pytest.raises(TypeError): - Argon2id( + clazz( salt=b"b" * 8, length=32, iterations=1, lanes=1, memory_cost=32, - ad="string", # type: ignore[arg-type] + ad="string", secret=None, ) with pytest.raises(TypeError): - Argon2id( + clazz( salt=b"b" * 8, length=32, iterations=1, lanes=1, memory_cost=32, ad=None, - secret="string", # type: ignore[arg-type] + secret="string", ) @pytest.mark.parametrize( @@ -110,10 +133,10 @@ def test_invalid_types(self, backend): (b"b" * 8, 32, 1, 32, 200), # memory_cost < 8 * lanes ], ) - def test_invalid_values(self, params, backend): + def test_invalid_values(self, clazz, params, backend): (salt, length, iterations, lanes, memory_cost) = params with pytest.raises(ValueError): - Argon2id( + clazz( salt=salt, length=length, iterations=iterations, @@ -121,16 +144,16 @@ def test_invalid_values(self, params, backend): memory_cost=memory_cost, ) - def test_already_finalized(self, backend): - argon2id = Argon2id( + def test_already_finalized(self, clazz, backend): + argon2id = clazz( salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 ) argon2id.derive(b"password") with pytest.raises(AlreadyFinalized): argon2id.derive(b"password") - def test_already_finalized_verify(self, backend): - argon2id = Argon2id( + def test_already_finalized_verify(self, clazz, backend): + argon2id = clazz( salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 ) digest = argon2id.derive(b"password") @@ -138,15 +161,15 @@ def test_already_finalized_verify(self, backend): argon2id.verify(b"password", digest) @pytest.mark.parametrize("digest", [b"invalidkey", b"0" * 32]) - def test_invalid_verify(self, digest, backend): - argon2id = Argon2id( + def test_invalid_verify(self, clazz, digest, backend): + argon2id = clazz( salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 ) with pytest.raises(InvalidKey): argon2id.verify(b"password", digest) - def test_verify(self, backend): - argon2id = Argon2id( + def test_verify(self, clazz, backend): + argon2id = clazz( salt=b"salt" * 2, length=32, iterations=1, @@ -156,7 +179,7 @@ def test_verify(self, backend): secret=None, ) digest = argon2id.derive(b"password") - Argon2id( + clazz( salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 ).verify(b"password", digest) @@ -178,19 +201,19 @@ def test_derive_phc_encoded(self, backend): "jFn1qYAgmfVKFWVeUGQcVK4d8RSiQJFTS7R7VII+fRk" ) - def test_verify_phc_encoded(self): + def test_verify_phc_encoded(self, clazz): # First generate a PHC string - argon2id = Argon2id( + argon2 = clazz( salt=b"0" * 8, length=32, iterations=1, lanes=1, memory_cost=32, ) - encoded = argon2id.derive_phc_encoded(b"password") + encoded = argon2.derive_phc_encoded(b"password") - Argon2id.verify_phc_encoded(b"password", encoded) - Argon2id( + clazz.verify_phc_encoded(b"password", encoded) + clazz( salt=b"0" * 8, length=32, iterations=1, @@ -199,7 +222,7 @@ def test_verify_phc_encoded(self): ).verify(b"password", base64.b64decode(encoded.split("$")[-1] + "=")) with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded(b"wrong_password", encoded) + clazz.verify_phc_encoded(b"wrong_password", encoded) def test_verify_phc_vector(self): # From https://github.com/P-H-C/phc-string-format/blob/master/phc-sf-spec.md#example @@ -209,62 +232,62 @@ def test_verify_phc_vector(self): secret=b"pepper", ) - def test_verify_phc_encoded_invalid_format(self): + def test_verify_phc_encoded_invalid_format(self, clazz): # Totally invalid string with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded(b"password", "not-a-valid-format") + clazz.verify_phc_encoded(b"password", "not-a-valid-format") # Invalid algorithm with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( - b"password", "$argon2i$v=19$m=32,t=1,p=1$c2FsdHNhbHQ$hash" + clazz.verify_phc_encoded( + b"password", "$krypton7$v=19$m=32,t=1,p=1$c2FsdHNhbHQ$hash" ) # Invalid version with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( + clazz.verify_phc_encoded( b"password", "$argon2id$v=18$m=32,t=1,p=1$c2FsdHNhbHQ$hash" ) # Missing parameters with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( + clazz.verify_phc_encoded( b"password", "$argon2id$v=19$m=32,t=1$c2FsdHNhbHQ$hash" ) # Parameters in wrong order with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( + clazz.verify_phc_encoded( b"password", "$argon2id$v=19$t=1,m=32,p=1$c2FsdHNhbHQ$hash" ) # Invalid memory cost with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( + clazz.verify_phc_encoded( b"password", "$argon2id$v=19$m=abc,t=1,p=1$!invalid!$hash" ) # Invalid iterations with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( + clazz.verify_phc_encoded( b"password", "$argon2id$v=19$m=32,t=abc,p=1$!invalid!$hash" ) # Invalid lanes with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( + clazz.verify_phc_encoded( b"password", "$argon2id$v=19$m=32,t=1,p=abc$!invalid!$hash" ) # Invalid base64 in salt with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( + clazz.verify_phc_encoded( b"password", "$argon2id$v=19$m=32,t=1,p=1$!invalid!$hash" ) # Invalid base64 in hash with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( + clazz.verify_phc_encoded( b"password", "$argon2id$v=19$m=32,t=1,p=1$c2FsdHNhbHQ$!invalid!", ) From ecf0f2a8b4eddd2ad7793784003eaeb5e09cc85d Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 20 Oct 2025 14:59:43 -0400 Subject: [PATCH 04/12] - Modify type hints to support py3.8 - Scope Argon2Variant enum to OpenSSL >=3.2 --- src/rust/src/backend/kdf.rs | 1 + tests/hazmat/primitives/test_argon2.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/rust/src/backend/kdf.rs b/src/rust/src/backend/kdf.rs index 03b7c17954c6..bef868550e3e 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -238,6 +238,7 @@ impl Scrypt { } } +#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] enum Argon2Variant { Argon2d, Argon2i, diff --git a/tests/hazmat/primitives/test_argon2.py b/tests/hazmat/primitives/test_argon2.py index bdb7e2dd58f7..3efc4270be0d 100644 --- a/tests/hazmat/primitives/test_argon2.py +++ b/tests/hazmat/primitives/test_argon2.py @@ -6,6 +6,7 @@ import base64 import binascii import os +from typing import List, Tuple import pytest @@ -23,7 +24,7 @@ variants = (Argon2d, Argon2i, Argon2id) -vectors: list[tuple[type, dict]] = [] +vectors: List[Tuple[type, dict]] = [] for clazz in variants: vectors.extend( (clazz, x) From 673c8347962590dbdf41cfea9c257c11fea906d8 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 20 Oct 2025 15:32:16 -0400 Subject: [PATCH 05/12] Updating docs for Argon2 family --- .../primitives/key-derivation-functions.rst | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/docs/hazmat/primitives/key-derivation-functions.rst b/docs/hazmat/primitives/key-derivation-functions.rst index 07d62b1682c4..41d4ad8f6c36 100644 --- a/docs/hazmat/primitives/key-derivation-functions.rst +++ b/docs/hazmat/primitives/key-derivation-functions.rst @@ -30,17 +30,36 @@ Different KDFs are suitable for different tasks such as: Variable cost algorithms ~~~~~~~~~~~~~~~~~~~~~~~~ -Argon2id --------- +Argon2 Family +------------- .. currentmodule:: cryptography.hazmat.primitives.kdf.argon2 +The Argon2 family of key derivation functions are designed for password storage and is described in :rfc:`9106`. +It consists of three variants that differ only how they access an internal memory buffer, which leads to different +trade-offs in resistance to hardware attacks. + +Each of the classes constructors and parameters are the same; only details of Argon2id are defined before, for brevity. + +.. class:: Argon2d(*, salt, length, iterations, lanes, memory_cost, ad=None, secret=None) + + .. versionadded:: 46.0.4 + + This variant of the Argon2 family maximizes resistance to time-memory-trade-off attacks, but introduces possible side-channels + + +.. class:: Argon2i(*, salt, length, iterations, lanes, memory_cost, ad=None, secret=None) + + .. versionadded:: 46.0.4 + + This variant of the Argon2 family resists side-channel attacks, but is vulernable to tim time-memory-trade-off attacks + + .. 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`. + Argon2id is a blend of the previous two variants. Argon2id should be used by most users, as recommended in :rfc:`9106`. This class conforms to the :class:`~cryptography.hazmat.primitives.kdf.KeyDerivationFunction` From 1f6e1e5feec3204cb21c00d9bd9cf04ef46fccf1 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 20 Oct 2025 15:41:47 -0400 Subject: [PATCH 06/12] Correcting typos --- docs/hazmat/primitives/key-derivation-functions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/hazmat/primitives/key-derivation-functions.rst b/docs/hazmat/primitives/key-derivation-functions.rst index 41d4ad8f6c36..460cddad05df 100644 --- a/docs/hazmat/primitives/key-derivation-functions.rst +++ b/docs/hazmat/primitives/key-derivation-functions.rst @@ -52,7 +52,7 @@ Each of the classes constructors and parameters are the same; only details of Ar .. versionadded:: 46.0.4 - This variant of the Argon2 family resists side-channel attacks, but is vulernable to tim time-memory-trade-off attacks + This variant of the Argon2 family resists side-channel attacks, but is vulnerable to time-memory-trade-off attacks .. class:: Argon2id(*, salt, length, iterations, lanes, memory_cost, ad=None, secret=None) From 07dd7a559bd56b022b8520b0ac22780b136afecd Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Tue, 21 Oct 2025 21:53:00 -0400 Subject: [PATCH 07/12] Rename base attribute and remove "allow(unused)" tag --- src/rust/src/backend/kdf.rs | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/rust/src/backend/kdf.rs b/src/rust/src/backend/kdf.rs index bef868550e3e..8bdaaedce408 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -542,20 +542,17 @@ impl BaseArgon2 { #[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] struct Argon2d { - #[allow(unused)] - base: BaseArgon2, + _base: BaseArgon2, } #[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] struct Argon2i { - #[allow(unused)] - base: BaseArgon2, + _base: BaseArgon2, } #[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] struct Argon2id { - #[allow(unused)] - base: BaseArgon2, + _base: BaseArgon2, } #[pyo3::pymethods] @@ -575,7 +572,7 @@ impl Argon2d { ) -> CryptographyResult { Ok({ Self { - base: BaseArgon2::new( + _base: BaseArgon2::new( py, salt, length, @@ -595,7 +592,7 @@ impl Argon2d { py: pyo3::Python<'p>, key_material: CffiBuf<'_>, ) -> CryptographyResult> { - self.base.derive(py, &Argon2Variant::Argon2d, key_material) + self._base.derive(py, &Argon2Variant::Argon2d, key_material) } #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] @@ -605,7 +602,7 @@ impl Argon2d { key_material: CffiBuf<'_>, expected_key: CffiBuf<'_>, ) -> CryptographyResult<()> { - self.base + self._base .verify(py, &Argon2Variant::Argon2d, key_material, expected_key) } @@ -615,7 +612,7 @@ impl Argon2d { py: pyo3::Python<'p>, key_material: CffiBuf<'_>, ) -> CryptographyResult> { - self.base + self._base .derive_phc_encoded(py, &Argon2Variant::Argon2d, key_material) } @@ -656,7 +653,7 @@ impl Argon2i { ) -> CryptographyResult { Ok({ Self { - base: BaseArgon2::new( + _base: BaseArgon2::new( py, salt, length, @@ -676,7 +673,7 @@ impl Argon2i { py: pyo3::Python<'p>, key_material: CffiBuf<'_>, ) -> CryptographyResult> { - self.base.derive(py, &Argon2Variant::Argon2i, key_material) + self._base.derive(py, &Argon2Variant::Argon2i, key_material) } #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] @@ -686,7 +683,7 @@ impl Argon2i { key_material: CffiBuf<'_>, expected_key: CffiBuf<'_>, ) -> CryptographyResult<()> { - self.base + self._base .verify(py, &Argon2Variant::Argon2i, key_material, expected_key) } @@ -696,7 +693,7 @@ impl Argon2i { py: pyo3::Python<'p>, key_material: CffiBuf<'_>, ) -> CryptographyResult> { - self.base + self._base .derive_phc_encoded(py, &Argon2Variant::Argon2i, key_material) } @@ -736,7 +733,7 @@ impl Argon2id { ) -> CryptographyResult { Ok({ Self { - base: BaseArgon2::new( + _base: BaseArgon2::new( py, salt, length, @@ -756,7 +753,8 @@ impl Argon2id { py: pyo3::Python<'p>, key_material: CffiBuf<'_>, ) -> CryptographyResult> { - self.base.derive(py, &Argon2Variant::Argon2id, key_material) + self._base + .derive(py, &Argon2Variant::Argon2id, key_material) } #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] @@ -766,7 +764,7 @@ impl Argon2id { key_material: CffiBuf<'_>, expected_key: CffiBuf<'_>, ) -> CryptographyResult<()> { - self.base + self._base .verify(py, &Argon2Variant::Argon2id, key_material, expected_key) } @@ -776,7 +774,7 @@ impl Argon2id { py: pyo3::Python<'p>, key_material: CffiBuf<'_>, ) -> CryptographyResult> { - self.base + self._base .derive_phc_encoded(py, &Argon2Variant::Argon2id, key_material) } From fe97b62b6e747047ea67872a97e26fc4e157b950 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 3 Nov 2025 11:24:14 -0500 Subject: [PATCH 08/12] Correct "versionadded" docstring for Argon2d and Argon2i --- docs/hazmat/primitives/key-derivation-functions.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/hazmat/primitives/key-derivation-functions.rst b/docs/hazmat/primitives/key-derivation-functions.rst index 460cddad05df..e676cb3c1b19 100644 --- a/docs/hazmat/primitives/key-derivation-functions.rst +++ b/docs/hazmat/primitives/key-derivation-functions.rst @@ -43,14 +43,14 @@ Each of the classes constructors and parameters are the same; only details of Ar .. class:: Argon2d(*, salt, length, iterations, lanes, memory_cost, ad=None, secret=None) - .. versionadded:: 46.0.4 + .. versionadded:: 47.0.0 This variant of the Argon2 family maximizes resistance to time-memory-trade-off attacks, but introduces possible side-channels .. class:: Argon2i(*, salt, length, iterations, lanes, memory_cost, ad=None, secret=None) - .. versionadded:: 46.0.4 + .. versionadded:: 47.0.0 This variant of the Argon2 family resists side-channel attacks, but is vulnerable to time-memory-trade-off attacks From e88dd946be8d042569f9a90ca4e2bb0b0c61830c Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 3 Nov 2025 11:25:58 -0500 Subject: [PATCH 09/12] Update error message to include helpful hint when using wrong Argon2 class --- src/rust/src/backend/kdf.rs | 25 ++++++++++++++++++++----- tests/hazmat/primitives/test_argon2.py | 8 ++++++++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/rust/src/backend/kdf.rs b/src/rust/src/backend/kdf.rs index 8bdaaedce408..e3e854512ddf 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -239,6 +239,7 @@ impl Scrypt { } #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] +#[derive(Debug, PartialEq)] enum Argon2Variant { Argon2d, Argon2i, @@ -446,16 +447,30 @@ impl BaseArgon2 { ) -> CryptographyResult<()> { let parts: Vec<_> = phc_encoded.split('$').collect(); - let variant_id: &str = match variant { - Argon2Variant::Argon2d => "argon2d", - Argon2Variant::Argon2i => "argon2i", - Argon2Variant::Argon2id => "argon2id", + if parts.len() != 6 || !parts[0].is_empty() { + return Err(CryptographyError::from(exceptions::InvalidKey::new_err( + "Invalid PHC string format.", + ))); + } + + let requested_variant: Option<&Argon2Variant> = match parts[1] { + "argon2d" => Some(&Argon2Variant::Argon2d), + "argon2i" => Some(&Argon2Variant::Argon2i), + "argon2id" => Some(&Argon2Variant::Argon2id), + _ => None, }; - if parts.len() != 6 || !parts[0].is_empty() || parts[1] != variant_id { + if requested_variant.is_none() { return Err(CryptographyError::from(exceptions::InvalidKey::new_err( "Invalid PHC string format.", ))); + } else if requested_variant.unwrap() != variant { + return Err(CryptographyError::from(exceptions::InvalidKey::new_err( + format!( + "Incorrect variant in PHC string, did you mean to use {:?}?", + requested_variant.unwrap() + ), + ))); } if parts[2] != "v=19" { diff --git a/tests/hazmat/primitives/test_argon2.py b/tests/hazmat/primitives/test_argon2.py index 3efc4270be0d..c369137a9164 100644 --- a/tests/hazmat/primitives/test_argon2.py +++ b/tests/hazmat/primitives/test_argon2.py @@ -244,6 +244,14 @@ def test_verify_phc_encoded_invalid_format(self, clazz): b"password", "$krypton7$v=19$m=32,t=1,p=1$c2FsdHNhbHQ$hash" ) + # Incorrect variant specified, offer a more helpful error message + wrong_variant = "argon2id" if clazz is not Argon2id else "argon2d" + with pytest.raises(InvalidKey, match="did you mean to use"): + clazz.verify_phc_encoded( + b"password", + f"${wrong_variant}$v=19$m=32,t=1,p=1$c2FsdHNhbHQ$!invalid!", + ) + # Invalid version with pytest.raises(InvalidKey): clazz.verify_phc_encoded( From c3f4297bd0ea5ed61235976d0da9fbf69a2658c3 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 3 Nov 2025 12:25:47 -0500 Subject: [PATCH 10/12] Update unit tests to exercise all Argon2 variants for derive_into --- tests/hazmat/primitives/test_argon2.py | 47 +++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/tests/hazmat/primitives/test_argon2.py b/tests/hazmat/primitives/test_argon2.py index 2b3bce586137..a5b096dd761f 100644 --- a/tests/hazmat/primitives/test_argon2.py +++ b/tests/hazmat/primitives/test_argon2.py @@ -51,9 +51,6 @@ def test_unsupported_backend(backend): skip_message="Argon2id not supported by this version of OpenSSL", ) class TestArgon2: - VECTORS = load_vectors_from_file( - os.path.join("KDF", "argon2id.txt"), load_nist_vectors - ) @pytest.fixture(scope="class", params=variants) def clazz(self, request) -> type: @@ -184,25 +181,27 @@ def test_verify(self, clazz, backend): salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 ).verify(b"password", digest) - def test_derive_into(self, backend): - argon2id = Argon2id( + def test_derive_into(self, clazz, backend): + argon2 = clazz( salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 ) buf = bytearray(32) - n = argon2id.derive_into(b"password", buf) + n = argon2.derive_into(b"password", buf) assert n == 32 # Verify the output matches what derive would produce - argon2id2 = Argon2id( + argon2_2 = clazz( salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 ) - expected = argon2id2.derive(b"password") + expected = argon2_2.derive(b"password") assert buf == expected @pytest.mark.parametrize( ("buflen", "outlen"), [(31, 32), (33, 32), (16, 32), (64, 32)] ) - def test_derive_into_buffer_incorrect_size(self, buflen, outlen, backend): - argon2id = Argon2id( + def test_derive_into_buffer_incorrect_size( + self, clazz, buflen, outlen, backend + ): + argon2 = clazz( salt=b"salt" * 2, length=outlen, iterations=1, @@ -211,16 +210,16 @@ def test_derive_into_buffer_incorrect_size(self, buflen, outlen, backend): ) buf = bytearray(buflen) with pytest.raises(ValueError, match="buffer must be"): - argon2id.derive_into(b"password", buf) + argon2.derive_into(b"password", buf) - def test_derive_into_already_finalized(self, backend): - argon2id = Argon2id( + def test_derive_into_already_finalized(self, clazz, backend): + argon2 = clazz( salt=b"salt" * 2, length=32, iterations=1, lanes=1, memory_cost=32 ) buf = bytearray(32) - argon2id.derive_into(b"password", buf) + argon2.derive_into(b"password", buf) with pytest.raises(AlreadyFinalized): - argon2id.derive_into(b"password2", buf) + argon2.derive_into(b"password2", buf) def test_derive_phc_encoded(self, backend): # Test that we can generate a PHC formatted string @@ -290,51 +289,53 @@ def test_verify_phc_encoded_invalid_format(self, clazz): f"${wrong_variant}$v=19$m=32,t=1,p=1$c2FsdHNhbHQ$!invalid!", ) + variant = clazz.__name__.lower() + # Invalid version with pytest.raises(InvalidKey): clazz.verify_phc_encoded( - b"password", "$argon2id$v=18$m=32,t=1,p=1$c2FsdHNhbHQ$hash" + b"password", f"${variant}$v=18$m=32,t=1,p=1$c2FsdHNhbHQ$hash" ) # Missing parameters with pytest.raises(InvalidKey): clazz.verify_phc_encoded( - b"password", "$argon2id$v=19$m=32,t=1$c2FsdHNhbHQ$hash" + b"password", f"${variant}$v=19$m=32,t=1$c2FsdHNhbHQ$hash" ) # Parameters in wrong order with pytest.raises(InvalidKey): clazz.verify_phc_encoded( - b"password", "$argon2id$v=19$t=1,m=32,p=1$c2FsdHNhbHQ$hash" + b"password", f"${variant}$v=19$t=1,m=32,p=1$c2FsdHNhbHQ$hash" ) # Invalid memory cost with pytest.raises(InvalidKey): clazz.verify_phc_encoded( - b"password", "$argon2id$v=19$m=abc,t=1,p=1$!invalid!$hash" + b"password", f"${variant}$v=19$m=abc,t=1,p=1$!invalid!$hash" ) # Invalid iterations with pytest.raises(InvalidKey): clazz.verify_phc_encoded( - b"password", "$argon2id$v=19$m=32,t=abc,p=1$!invalid!$hash" + b"password", f"${variant}$v=19$m=32,t=abc,p=1$!invalid!$hash" ) # Invalid lanes with pytest.raises(InvalidKey): clazz.verify_phc_encoded( - b"password", "$argon2id$v=19$m=32,t=1,p=abc$!invalid!$hash" + b"password", f"${variant}$v=19$m=32,t=1,p=abc$!invalid!$hash" ) # Invalid base64 in salt with pytest.raises(InvalidKey): clazz.verify_phc_encoded( - b"password", "$argon2id$v=19$m=32,t=1,p=1$!invalid!$hash" + b"password", f"${variant}$v=19$m=32,t=1,p=1$!invalid!$hash" ) # Invalid base64 in hash with pytest.raises(InvalidKey): clazz.verify_phc_encoded( b"password", - "$argon2id$v=19$m=32,t=1,p=1$c2FsdHNhbHQ$!invalid!", + f"${variant}$v=19$m=32,t=1,p=1$c2FsdHNhbHQ$!invalid!", ) From 2bd835ba55912c12876cb67311eff8ec157657a0 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 3 Nov 2025 12:28:57 -0500 Subject: [PATCH 11/12] Removing empty spacing to appease ruff --- tests/hazmat/primitives/test_argon2.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/hazmat/primitives/test_argon2.py b/tests/hazmat/primitives/test_argon2.py index a5b096dd761f..edc343476af6 100644 --- a/tests/hazmat/primitives/test_argon2.py +++ b/tests/hazmat/primitives/test_argon2.py @@ -51,7 +51,6 @@ def test_unsupported_backend(backend): skip_message="Argon2id not supported by this version of OpenSSL", ) class TestArgon2: - @pytest.fixture(scope="class", params=variants) def clazz(self, request) -> type: return request.param From c0b0b61d923ed7aac53da2b692e69d920f1505d1 Mon Sep 17 00:00:00 2001 From: greateggsgreg Date: Mon, 3 Nov 2025 12:43:06 -0500 Subject: [PATCH 12/12] Add derive_into interface definitions for additional Argon2 variants --- src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi index 07880d8ed648..29d380ab214f 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi @@ -47,6 +47,7 @@ class Argon2d: secret: bytes | None = None, ) -> None: ... def derive(self, key_material: bytes) -> bytes: ... + def derive_into(self, key_material: bytes, buffer: Buffer) -> int: ... def verify(self, key_material: bytes, expected_key: bytes) -> None: ... def derive_phc_encoded(self, key_material: bytes) -> str: ... @classmethod @@ -67,6 +68,7 @@ class Argon2i: secret: bytes | None = None, ) -> None: ... def derive(self, key_material: bytes) -> bytes: ... + def derive_into(self, key_material: bytes, buffer: Buffer) -> int: ... def verify(self, key_material: bytes, expected_key: bytes) -> None: ... def derive_phc_encoded(self, key_material: bytes) -> str: ... @classmethod