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

KeyringMasterKeyProvider

Provider whose former process cache is replaced by this session.

FileFallbackMasterKeyProvider

File-backed provider whose unlocked buffers are session-scoped.

2026-05-14-profile-bucket-lifecycle-adr

Decision that made unlocked key material bucket-session-owned.

class BucketSession(*, bucket_id, kek_buffer, dek_buffer, idle_window, idle_deadline, unsecured_backend)[source]

Bases: object

One 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:
  • bucket_id (str)

  • kek_buffer (bytearray)

  • dek_buffer (bytearray)

  • idle_window (timedelta)

  • idle_deadline (datetime)

  • unsecured_backend (bool)

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) – When True, the session was opened against an unsecured (non-OS-keychain) backend; callers use this flag to emit appropriate warnings.

Return type:

BucketSession

Returns:

A new BucketSession with the provided credentials and TTL.

Raises:

StorageValidationError – When bucket_id is empty, idle_minutes is not positive, kek is not 32 bytes, or dek is not 32 bytes.

property bucket_id: str
property sealed: bool
property unsecured_backend: bool
property idle_deadline: datetime
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.

touch(now)[source]

Reset the idle-timeout deadline to now + idle_window.

Return type:

None

Parameters:

now (datetime)

is_expired(now)[source]

Return whether the idle window has elapsed at now.

Return type:

bool

Parameters:

now (datetime)

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) – The Settings routing to this bucket’s database, passed through to get_engine().

Return type:

Engine

Returns:

The Engine bound 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 next acquire_engine() call returns the stale handle bound to a directory that no longer exists. This is the session-scoped counterpart of dispose_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:

None

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:

None