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:
CadrumoErrorRaised when a passphrase change cannot be honoured as supplied.
- Parameters:
message (str | None)
translated_message (str | None)
password_refusal (ProspectiveProfilePasswordRefusal | None)
- Return type:
None
- property password_refusal: ProspectiveProfilePasswordRefusal | None¶
Retain the typed refusal for trusted in-process consumers only.
- code¶
- class ProfilePassphraseRotationOutcome(**data)[source]¶
Bases:
BaseModelTyped 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 undernew_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 equalnew_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:
- Returns:
A
ProfilePassphraseRotationOutcomenaming 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
dekthrough 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:
- Returns:
The committed replacement envelope.
- Parameters:
profile_id (UUID)
dek (bytes)
current (ProfileCustodyEnvelopePort)
new_passphrase (str)
storage_root (Path)
profile_decode_context (ProfileDecodeContext)