Source code for cadrumo.adapters.persistence.storage.master_key.kdf_params

"""Canonical Argon2id parameter record and the window it validates against.

Strict pydantic v2 model declaring the Argon2id KEK-derivation parameters
the substrate accepts. The :meth:`KdfParams.default` classmethod
materialises the OWASP 2024 Password Storage Cheat Sheet baseline:

- ``algorithm`` = ``"argon2id"``
- ``version`` = ``19`` (Argon2 v1.3)
- ``memory_cost`` = ``19 * 1024`` KiB (19 MiB)
- ``time_cost`` = ``2`` iterations
- ``parallelism`` = ``1`` lane
- ``salt`` = 16 bytes
- ``output_length`` = 32 bytes

Validators reject parameter sets outside the supported window, so no
supplied parameter set can drive the KDF into a weaker regime at unlock.

The window constants live here rather than in a package-level module of
their own. They were separated originally because a second, manifest-side
record declared the same axes and could not import this package without
closing a cycle. Both that record and the on-disk key store it validated
have since been deleted with the shared-master surface, leaving this record
as the window's only reader.

Widening the window is a deliberate edit here; a *cost bump* raises a
store's enrolled parameters within it and stays non-breaking.

This window is a KIBIBYTE, continuously-bounded one, deliberately distinct
from the finite mebibyte grid profile custody enrols under: that grid is a
strictly narrower shape, not a wider one, and every value an already-enrolled
store carries must stay readable here.
"""

from __future__ import annotations

import secrets
from typing import Final, Literal, get_args

from pydantic import BaseModel, Field, field_serializer, field_validator

from .....core.errors.hierarchy import pydantic_validation_boundary
from .....core.models import STRICT_FROZEN_CONFIG as _STRICT_FROZEN
from .._kdf_salt import KDF_SALT_BYTES, decode_kdf_salt, encode_kdf_salt, require_kdf_salt_length
from ..errors import StorageValidationError

#: Argon2 version 1.3, the only version this substrate derives under.
Argon2Version = Literal[19]

#: Derived KEK length in bytes, fixed by AES-256-GCM.
KdfOutputLength = Literal[32]

# The two single-valued axes are declared ONCE, as the types above, and their
# runtime values read back out. Writing the annotation and the constant
# separately would put the same number in two places.
ARGON2_VERSION: Final = get_args(Argon2Version)[0]
KDF_OUTPUT_BYTES: Final = get_args(KdfOutputLength)[0]

#: OWASP 2024 Password Storage Cheat Sheet baseline, and the floor.
MIN_MEMORY_COST_KIB: Final = 19 * 1024
MAX_MEMORY_COST_KIB: Final = 1024 * 1024
MIN_TIME_COST: Final = 2
MAX_TIME_COST: Final = 16
MIN_PARALLELISM: Final = 1
MAX_PARALLELISM: Final = 8

_SALT_BYTES = KDF_SALT_BYTES


[docs] class KdfParams(BaseModel): """OWASP-baseline Argon2id parameters with strict validation. The constructor for a new enrolment. It reads the window declared above rather than restating one. It once shared that window with an on-disk record validating the same axes; that record is deleted, so the window now has a single reader and the agreement it enforced is no longer at risk. """ model_config = _STRICT_FROZEN algorithm: Literal["argon2id"] version: Argon2Version memory_cost: int = Field(ge=MIN_MEMORY_COST_KIB, le=MAX_MEMORY_COST_KIB) time_cost: int = Field(ge=MIN_TIME_COST, le=MAX_TIME_COST) parallelism: int = Field(ge=MIN_PARALLELISM, le=MAX_PARALLELISM) salt: bytes output_length: KdfOutputLength @field_validator("salt") @classmethod @pydantic_validation_boundary def _check_salt_length(cls, value: bytes) -> bytes: return require_kdf_salt_length(value, error_type=StorageValidationError) @field_serializer("salt") def _serialise_salt(self, value: bytes) -> str: return encode_kdf_salt(value) @field_validator("salt", mode="before") @classmethod @pydantic_validation_boundary def _decode_salt(cls, value: object) -> bytes: return decode_kdf_salt(value, error_type=StorageValidationError)
[docs] @classmethod def default(cls) -> KdfParams: """Return a :class:`KdfParams` instance with the canonical OWASP 2024 Argon2id baseline parameters.""" return cls( algorithm="argon2id", version=ARGON2_VERSION, memory_cost=MIN_MEMORY_COST_KIB, time_cost=MIN_TIME_COST, parallelism=MIN_PARALLELISM, salt=secrets.token_bytes(_SALT_BYTES), output_length=KDF_OUTPUT_BYTES, )
__all__ = [ "ARGON2_VERSION", "KDF_OUTPUT_BYTES", "MAX_MEMORY_COST_KIB", "MAX_PARALLELISM", "MAX_TIME_COST", "MIN_MEMORY_COST_KIB", "MIN_PARALLELISM", "MIN_TIME_COST", "Argon2Version", "KdfOutputLength", "KdfParams", ]