diff --git a/docs/hazmat/primitives/key-derivation-functions.rst b/docs/hazmat/primitives/key-derivation-functions.rst index d3d186e51fa8..f8228c6d9586 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:: 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:: 47.0.0 + + 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) .. 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` diff --git a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi index 92c0f03d5546..29d380ab214f 100644 --- a/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi +++ b/src/cryptography/hazmat/bindings/_rust/openssl/kdf.pyi @@ -34,6 +34,48 @@ class Scrypt: def derive_into(self, key_material: Buffer, buffer: Buffer) -> int: ... 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 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 + 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 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 + 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 cc39214a1fe2..b151c8c3f3bd 100644 --- a/src/rust/src/backend/kdf.rs +++ b/src/rust/src/backend/kdf.rs @@ -301,8 +301,15 @@ impl Scrypt { } } -#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] -struct Argon2id { +#[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] +#[derive(Debug, PartialEq)] +enum Argon2Variant { + Argon2d, + Argon2i, + Argon2id, +} + +struct BaseArgon2 { #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] salt: pyo3::Py, #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] @@ -321,11 +328,12 @@ struct Argon2id { used: bool, } -impl Argon2id { +impl BaseArgon2 { #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] fn derive_into_buffer( &mut self, py: pyo3::Python<'_>, + variant: &Argon2Variant, key_material: &[u8], output: &mut [u8], ) -> CryptographyResult { @@ -343,7 +351,13 @@ impl Argon2id { )); } - openssl::kdf::argon2id( + let derive_fn = match &variant { + Argon2Variant::Argon2d => openssl::kdf::argon2d, + Argon2Variant::Argon2i => openssl::kdf::argon2i, + Argon2Variant::Argon2id => openssl::kdf::argon2id, + }; + + (derive_fn)( None, key_material, self.salt.as_bytes(py), @@ -358,12 +372,7 @@ impl Argon2id { Ok(self.length) } -} -#[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<'_>, @@ -388,14 +397,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" ), )); } @@ -437,8 +446,7 @@ impl Argon2id { )); } - - Ok(Argon2id{ + Ok(Self{ salt, length, iterations, @@ -452,24 +460,15 @@ impl Argon2id { } } - #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] - fn derive_into( - &mut self, - py: pyo3::Python<'_>, - key_material: CffiBuf<'_>, - mut buf: CffiMutBuf<'_>, - ) -> CryptographyResult { - self.derive_into_buffer(py, key_material.as_bytes(), buf.as_mut_bytes()) - } - #[cfg(CRYPTOGRAPHY_OPENSSL_320_OR_GREATER)] fn derive<'p>( &mut self, py: pyo3::Python<'p>, + variant: &Argon2Variant, key_material: CffiBuf<'_>, ) -> CryptographyResult> { Ok(pyo3::types::PyBytes::new_with(py, self.length, |output| { - self.derive_into_buffer(py, key_material.as_bytes(), output)?; + self.derive_into_buffer(py, variant, key_material.as_bytes(), output)?; Ok(()) })?) } @@ -478,10 +477,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(); @@ -498,40 +498,66 @@ 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" { + 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 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" { return Err(CryptographyError::from(exceptions::InvalidKey::new_err( "Invalid version in PHC string.", @@ -590,7 +616,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(), @@ -601,7 +627,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) { @@ -614,6 +640,308 @@ impl Argon2id { } } +#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] +struct Argon2d { + _base: BaseArgon2, +} + +#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] +struct Argon2i { + _base: BaseArgon2, +} + +#[pyo3::pyclass(module = "cryptography.hazmat.primitives.kdf.argon2")] +struct Argon2id { + _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_into( + &mut self, + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + mut buf: CffiMutBuf<'_>, + ) -> CryptographyResult { + self._base.derive_into_buffer( + py, + &Argon2Variant::Argon2d, + key_material.as_bytes(), + buf.as_mut_bytes(), + ) + } + + #[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_into( + &mut self, + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + mut buf: CffiMutBuf<'_>, + ) -> CryptographyResult { + self._base.derive_into_buffer( + py, + &Argon2Variant::Argon2i, + key_material.as_bytes(), + buf.as_mut_bytes(), + ) + } + + #[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_into( + &mut self, + py: pyo3::Python<'_>, + key_material: CffiBuf<'_>, + mut buf: CffiMutBuf<'_>, + ) -> CryptographyResult { + self._base.derive_into_buffer( + py, + &Argon2Variant::Argon2id, + key_material.as_bytes(), + buf.as_mut_bytes(), + ) + } + + #[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, @@ -1370,6 +1698,7 @@ impl ConcatKdfHmac { pub(crate) mod kdf { #[pymodule_export] use super::{ - Argon2id, ConcatKdfHash, ConcatKdfHmac, Hkdf, HkdfExpand, Pbkdf2Hmac, Scrypt, X963Kdf, + Argon2d, Argon2i, Argon2id, ConcatKdfHash, ConcatKdfHmac, Hkdf, HkdfExpand, Pbkdf2Hmac, + Scrypt, X963Kdf, }; } diff --git a/tests/hazmat/primitives/test_argon2.py b/tests/hazmat/primitives/test_argon2.py index ef2cc0cedd96..edc343476af6 100644 --- a/tests/hazmat/primitives/test_argon2.py +++ b/tests/hazmat/primitives/test_argon2.py @@ -6,20 +6,33 @@ import base64 import binascii import os +from typing import List, Tuple 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 +50,16 @@ 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: + @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 +74,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 +83,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 +98,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 +130,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 +141,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 +158,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,29 +176,31 @@ 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) - 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, @@ -187,16 +209,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 @@ -216,19 +238,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, @@ -237,7 +259,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 @@ -247,62 +269,72 @@ 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" + ) + + # 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!", ) + variant = clazz.__name__.lower() + # Invalid version with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( - b"password", "$argon2id$v=18$m=32,t=1,p=1$c2FsdHNhbHQ$hash" + clazz.verify_phc_encoded( + b"password", f"${variant}$v=18$m=32,t=1,p=1$c2FsdHNhbHQ$hash" ) # Missing parameters with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( - b"password", "$argon2id$v=19$m=32,t=1$c2FsdHNhbHQ$hash" + clazz.verify_phc_encoded( + b"password", f"${variant}$v=19$m=32,t=1$c2FsdHNhbHQ$hash" ) # Parameters in wrong order with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( - b"password", "$argon2id$v=19$t=1,m=32,p=1$c2FsdHNhbHQ$hash" + clazz.verify_phc_encoded( + b"password", f"${variant}$v=19$t=1,m=32,p=1$c2FsdHNhbHQ$hash" ) # Invalid memory cost with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( - b"password", "$argon2id$v=19$m=abc,t=1,p=1$!invalid!$hash" + clazz.verify_phc_encoded( + b"password", f"${variant}$v=19$m=abc,t=1,p=1$!invalid!$hash" ) # Invalid iterations with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( - b"password", "$argon2id$v=19$m=32,t=abc,p=1$!invalid!$hash" + clazz.verify_phc_encoded( + b"password", f"${variant}$v=19$m=32,t=abc,p=1$!invalid!$hash" ) # Invalid lanes with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( - b"password", "$argon2id$v=19$m=32,t=1,p=abc$!invalid!$hash" + clazz.verify_phc_encoded( + b"password", f"${variant}$v=19$m=32,t=1,p=abc$!invalid!$hash" ) # Invalid base64 in salt with pytest.raises(InvalidKey): - Argon2id.verify_phc_encoded( - b"password", "$argon2id$v=19$m=32,t=1,p=1$!invalid!$hash" + clazz.verify_phc_encoded( + b"password", f"${variant}$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!", + f"${variant}$v=19$m=32,t=1,p=1$c2FsdHNhbHQ$!invalid!", )