cadrumo.adapters.persistence.storage.custody.acceleration_receipt module

Profile acceleration receipt: cross-process custody of a wrapped bucket DEK.

This is NOT a session. It has no counterparty and no protocol: it is a locally held wrap of an already-unlocked DEK, carrying a deadline, that lets a later process skip the passphrase. The AEAT authority session – an encrypted row inside the bucket, revocable only with the key – is the artefact that owns the word “session” in this codebase, and conflating the two has produced a wrong architectural premise more than once. The “logged in” state minted by aeat config login survives across CLI processes as two split-knowledge artefacts, either of which is useless alone:

  • an ephemeral 32-byte session key held ONLY in the OS keychain under the service cadrumo:profile-session:v2 with the immutable profile UUID plus minted session UUID as account, and

  • an on-disk session.v2.json record in the separated profile keystore directory carrying the AES-256-GCM wrap of the bucket DEK under that session key, with EVERY metadata field (schema version, immutable profile UUID, random session UUID, custody generation, DEK epoch, issued_at, the sliding idle deadline, and immutable absolute deadline) bound as AEAD associated data.

A disk-only attacker sees ciphertext no more revealing than the already-persisted wrapped bucket.dek.json; a keychain-only attacker holds a random key with nothing to decrypt; altering any persisted deadline breaks the GCM tag. Resume evaluation is FAIL-CLOSED: an expired, tampered, version-mismatched, or keychain-orphaned record is deleted and refused with a typed ProfileSessionRefusalReason, never silently tolerated. No plaintext KEK, DEK, or session-key byte ever lands on disk.

Zeroisation honesty: the session key and DEK are held in bytearray buffers wiped through zeroise() on every exit path, but the AEAD primitives and the pydantic boundary require transient immutable bytes views whose lifetime the garbage collector owns — the same best-effort contract BucketSession documents for the live in-process buffers.

Keychain failures are normalised to KeyringUnavailableError on every path, including failures raised OUTSIDE the keyring library’s own exception hierarchy. A backend can pass the class-level usability probe and still fail at call time: the Windows credential store raises win32ctypes.pywin32.pywintypes.error (for example WinError 1312, “a specified logon session does not exist”) from a process whose logon session the credential manager cannot reach, and that type derives directly from Exception – it is neither a KeyringError nor an OSError, so a guard naming either lets it escape as a raw traceback. Normalising here is what makes the documented degradation reachable: no persisted artefact, a process-scoped login, and a warning to the operator. The error type is caught structurally rather than imported by name so this module carries no platform-specific import.

See also

BucketSession

The in-process materialisation this persisted record re-opens.

PROFILE_SESSION_KEYCHAIN_SERVICE: Final[str]

OS-keychain service name for current profile acceleration-receipt keys.

The token deliberately lags the concept name. It addresses entries in the OS credential store, OUTSIDE the storage root, so a rename orphans every existing entry: deleting the storage root cannot reap them, and code that deleted under the old token first would be exactly the migration path this project forbids. Permanent credential residue on real machines is the worse outcome, so the wire identifier stays fixed while the code name says what the artefact is.

PROFILE_SESSION_RECORD_MAX_BYTES: Final[int]

Strict ceiling for one canonical session.v2.json receipt.

class ProfileSessionResumeOutcome(**data)[source]

Bases: BaseModel

Typed outcome of a fail-closed persisted-session resume evaluation.

Never carries key material: the resumed DEK travels beside this record as a separate return value so no pydantic dump can surface it.

Parameters:

data (Any)

resumed: bool
refusal: ProfileSessionRefusalReason | None
record: _crypto.PersistedProfileSession | None
profile_session_path(*, storage_root, profile_id)[source]

Return the locator consumed exclusively by custody local-record operations.

Return type:

Path

Parameters:
exception AccelerationReceiptRevocationError(message=None, *, context=None, translated_message=None)[source]

Bases: StorageError

Raised when a receipt survived the revocation that was asked to remove it.

Deliberately NOT a subclass of the custody record error this module already catches and logs, because the point is that it must not be swallowed by the same handler.

A boolean return would have repeated the defect one level up: the caller that ignored the value is precisely the caller that reports the profile closed. The revocation entry point is the single authority for “this profile is no longer logged in”, so when it cannot prove the receipt is gone it must not return as though it had.

Parameters:
  • message (str | None)

  • context (Mapping[str, object] | None)

  • translated_message (str | None)

Return type:

None

code
delete_profile_session(*, storage_root, profile_id)[source]

Revoke a profile receipt under the custody-wide session lifecycle lock.

Return type:

None

Parameters:
mint_profile_session(*, storage_root, profile_id, custody_generation, dek_epoch, dek, now, idle_minutes, absolute_minutes)[source]

Mint a profile receipt under the custody-wide session lifecycle lock.

Validate all deterministic receipt inputs before opening the custody root. A rejected bootstrap call must not provision a durable lock leaf merely to report malformed identity, metadata, key material, or time windows.

Return type:

PersistedProfileSession

Parameters:
resume_profile_session(*, storage_root, profile_id, custody_generation, dek_epoch, now)[source]

Evaluate a provisioned profile receipt under the custody-wide lifecycle lock.

The active-profile transaction provisions the root lock before a profile can reach this resume path. A raw first call against an unprovisioned storage root is therefore bootstrap work, not an observational refusal; established logged-out profiles take the lock without changing their session or retirement leaves.

Return type:

tuple[ProfileSessionResumeOutcome, bytearray | None]

Parameters:
advance_persisted_profile_session_idle_deadline(*, storage_root, profile_id, record, new_idle_deadline)[source]

Advance a profile receipt under the custody-wide session lifecycle lock.

Return type:

PersistedProfileSession

Parameters: