aeat.adapters.persistence.storage.master_key._bucket_session module¶
Per-bucket instance-scoped unlock state.
BucketSession replaces the module-global ClassVar caches that previously survived a bucket switch on KeyringMasterKeyProvider and FileFallbackMasterKeyProvider. Each instance binds to exactly one bucket_id; the unlocked KEK and DEK are held in bytearray buffers so close() can overwrite the bytes in place before the references are dropped. The session is the only object that holds cleartext key material on the master-key surface; the substrate invariant forbids any module-global mutable state that could survive a bucket switch.
The bytearray zeroisation is best-effort. Python may have produced short-lived bytes copies of the buffers when callers materialised the kek / dek properties; the garbage collector owns the lifetime of those copies. The contract is documented honestly so callers do not assume Python guarantees a deeper wipe than the language can deliver.
See also
KeyringMasterKeyProviderProvider whose former process cache is replaced by this session.
FileFallbackMasterKeyProviderFile-backed provider whose unlocked buffers are session-scoped.
2026-05-14-profile-bucket-lifecycle-adrDecision that made unlocked key material bucket-session-owned.
- class BucketSession(*, bucket_id, kek_buffer, dek_buffer, idle_window, idle_deadline, unsecured_backend)[source]¶
Bases:
objectOne per-bucket unlock session.
The class deliberately holds NO ClassVar mutable state. Two sessions for two distinct bucket ids own two independent bytearray buffers and never share key material.
- Parameters:
- classmethod open(*, bucket_id, kek, dek, idle_minutes, opened_at, unsecured_backend=False)[source]¶
Open a session for one bucket.
- Parameters:
bucket_id (
str) – Non-empty identifier of the bucket being unlocked.kek (
bytes) – 32-byte Argon2id-derived key-encryption key.dek (
bytes) – 32-byte data-encryption key recovered by unwrapping the bucket’s wrapped DEK under the KEK.idle_minutes (
int) – Idle-timeout window in minutes; must be a strict positive integer.opened_at (
datetime) – UTC timestamp at which the session opened.unsecured_backend (
bool) – WhenTrue, the session was opened against an unsecured (non-OS-keychain) backend; callers use this flag to emit appropriate warnings.
- Return type:
- Returns:
A new
BucketSessionwith the provided credentials and TTL.- Raises:
StorageValidationError – When
bucket_idis empty,idle_minutesis not positive,kekis not 32 bytes, ordekis not 32 bytes.
- property kek: bytes¶
Return an immutable view of the live KEK bytes.
Raises BucketLockedError after close() has sealed the session.
- property dek: bytes¶
Return an immutable view of the live DEK bytes.
Raises BucketLockedError after close() has sealed the session.
- acquire_engine(settings)[source]¶
Lazily acquire and register this bucket’s engine on first storage access.
The session is the single owner of the SQLAlchemy engine lifecycle for its bucket: the first storage access within the session resolves (or creates) the bucket engine and registers the handle here, so
close()disposes exactly that engine on session close or profile switch. Subsequent accesses return the already-registered handle.- Parameters:
settings (
Settings) – TheSettingsrouting to this bucket’s database, passed through toget_engine().- Return type:
- Returns:
The
Enginebound to this session.- Raises:
BucketLockedError – When the session has already been sealed.
- invalidate_engine()[source]¶
Drop this session’s cached engine handle without sealing the session.
acquire_engine()caches its resolved handle for the life of the session, so a caller that destroys and later re-materialises this bucket’s on-disk database out from under a still-open session (the profile-reset / bucket-removal path) must invalidate the session-level cache too, or the nextacquire_engine()call returns the stale handle bound to a directory that no longer exists. This is the session-scoped counterpart ofdispose_engines_for_bucket(), which only evicts the process-wide engine cache; callers that remove a bucket directory while its session may still be active must call both. A no-op when no engine has been acquired yet or the session is already sealed.- Return type:
- close()[source]¶
Zeroise key buffers, dispose the bucket’s engine, and seal the session.
Idempotent: a second call after the first is a no-op.
Engine disposal:
The session owns the engine lifecycle for its bucket. Closing the session (on idle expiry, profile switch, or explicit close) disposes the engine handle registered at first storage access and evicts every cached engine bound to this bucket id, so the next consumer that opens a different bucket — or re-opens this one — never reuses a stale engine handle. Disposal keys on bucket identity, so it never depends on re-deriving a database route from live settings.
- Return type: