cadrumo.adapters.persistence.storage.recovery_key module

Recovery-code minting and its canonical grouped-code encoder.

A recovery code is an opaque high-entropy secret as far as every consumer is concerned; this module is the only thing in the substrate that can mint one strong enough to resist offline guessing. A profile’s recovery envelope wraps that profile’s DEK under its own supervised Argon2id parameters and takes the canonical code as its secret. The encoder is bound to no custody architecture, no file layout and no key schedule, and the substrate never persists the code.

Shape. Six groups of five symbols drawn from a 32-symbol alphabet that omits the characters people misread (0/O, 1/I/L), joined with hyphens: XXXXX-XXXXX-XXXXX-XXXXX-XXXXX-XXXXX. Thirty symbols at five bits each carry 150 bits of entropy, which is more than the 128 bits behind the password envelope’s own key. Operator input is normalised before proof: case, hyphens and whitespace are cosmetic, so a code typed in lowercase without separators proves possession exactly as the displayed form does.

Wipeable key material. The code is held in a bytearray rather than an immutable str, so the substrate’s zeroise() primitive can overwrite it in place once the operator has copied it down. The honest limit is the one the session path already discloses: reading RecoveryKey.code materialises a transient str copy whose lifetime the garbage collector owns. What this module does not do is hold the only copy of a secret in a form no wipe primitive can reach.

class RecoveryKey(*, code)[source]

Bases: object

Wipeable container for one canonical recovery code.

Deliberately not a pydantic model. The sibling BucketSession sets the precedent for live key material: a slotted plain class keeps the buffer mutable, and – because there is no model_dump_json – makes it structurally impossible to serialise the secret by accident.

Parameters:

code (str)

property code: str

Return the canonical grouped code, decoded from its wipeable buffer.

wipe()[source]

Overwrite the code buffer with zero bytes.

Idempotent: wiping an already-wiped key is a no-op that leaves the buffer zeroed and its length unchanged.

Return type:

None

normalise_recovery_code(text)[source]

Reduce operator input to the bare symbol run the code was minted from.

Case, hyphens, underscores and whitespace are cosmetic and dropped. Any other character, or a run that is not exactly the minted length, is refused: a code that cannot be the minted one must not reach the KDF.

Raises:

StorageValidationError – When text cannot be a recovery code.

Return type:

str

Parameters:

text (str)

format_recovery_code(symbols)[source]

Render a bare symbol run in the canonical hyphen-grouped form.

Return type:

str

Parameters:

symbols (str)

canonical_recovery_code(text)[source]

Return the canonical form of an operator-supplied code, or refuse it.

Return type:

str

Parameters:

text (str)

generate_recovery_key()[source]

Mint a fresh RecoveryKey from the process CSPRNG.

Each symbol is drawn independently with secrets.choice(), so the code carries the full five bits per symbol. The returned record is the only in-memory copy; callers must arrange for the operator to copy the code down, then RecoveryKey.wipe() it.

Return type:

RecoveryKey