cadrumo.application.user_profile.passphrase_rotation module

Change a profile’s password without changing the key it protects.

Rotation re-mints the password envelope over the SAME data-encryption key. That distinction is the whole design: a re-key would have to re-encrypt every record the profile holds and would invalidate the committed sentinel and any recovery code the operator is keeping, while a re-wrap touches exactly one file and leaves every other custody fact standing.

Two invariants make that safe, and both are enforced rather than assumed.

The DEK epoch is preserved. The committed sentinel and the recovery wrapper are bound to (profile_id, dek_epoch) and to neither the password envelope’s digest nor its generation, so holding the epoch keeps an already-issued recovery code working. Minting a fresh epoch here would silently destroy the only second door a taxpayer holds, at the moment they change their password and with no error to show for it.

The record row is re-headed in the same span. A row’s write provenance folds the envelope digest and the password generation, so a bare envelope swap leaves a profile that authenticates under the new password and then cannot read its own record.

See also

register_profile_with_credentials()

The creation door, which mints the first envelope this one replaces.

exception ProfilePassphraseRotationError(message=None, *, context=None, translated_message=None, password_refusal=None)[source]

Bases: CadrumoError

Raised when a passphrase change cannot be honoured as supplied.

Parameters:
Return type:

None

property password_refusal: ProspectiveProfilePasswordRefusal | None

Retain the typed refusal for trusted in-process consumers only.

code
class ProfilePassphraseRotationOutcome(**data)[source]

Bases: BaseModel

Typed result of one completed rotation.

Carries no key material and no passphrase: only the non-secret facts a surface needs to tell the operator what changed and what did not.

Parameters:

data (Any)

profile_id: ProfileId
password_generation: int
dek_epoch_preserved: bool
recovery_enrollment_retained: bool
rotate_profile_passphrase(*, profile_id, current_passphrase, new_passphrase, new_passphrase_confirmation, root=None, profile_decode_context)[source]

Re-wrap profile_id’s data key under new_passphrase.

Fails closed at every step before the swap: a wrong current passphrase, a new one outside the profile-password contract, or a confirmation that does not match all refuse with the committed envelope untouched and still usable.

The confirmation is compared here as well as at whatever surface collected it. A caller reaching this function directly must not be able to skip the check simply by not having a command line.

Parameters:
  • profile_id (UUID) – The profile whose password wrapper to replace.

  • current_passphrase (str) – Proof of the existing credential. Never logged.

  • new_passphrase (str) – The replacement credential. Must satisfy the canonical profile-password contract.

  • new_passphrase_confirmation (str) – Must equal new_passphrase.

  • root (Path | None) – Storage root override; the effective root when omitted.

  • profile_decode_context (ProfileDecodeContext) – Decode context supplied by the enclosing pinned authority operation for the authenticated record.

Return type:

ProfilePassphraseRotationOutcome

Returns:

A ProfilePassphraseRotationOutcome naming the new generation.

Raises:

ProfilePassphraseRotationError – When the confirmation does not match, the new passphrase is invalid, or the current one does not open the committed envelope.

rewrap_profile_passphrase_under_lock(*, profile_id, dek, current, new_passphrase, storage_root, profile_decode_context)[source]

Re-head the record and swap the password envelope over an already-proven DEK.

The shared second half of every passphrase replacement. The caller holds the custody transaction lock and has proven dek through whichever door authorises the change: the current passphrase for a rotation, or the enrolled recovery code for a reset. Nothing here re-proves anything, so this must not be reachable from a surface that has not.

Return type:

ProfileCustodyEnvelopePort

Returns:

The committed replacement envelope.

Parameters: