exlab_wizard.lims.keyring_store#

OS keyring storage with encrypted-at-rest fallback. Backend Spec §7.4.

Two backends, automatic dispatch:

  1. OS keyring – the keyring package routes to Keychain (macOS), Credential Manager (Windows), or Secret Service (Linux desktop) per §7.4. This is the preferred path because the OS already owns lifecycle (encryption-at-rest, scoping to the OS user).

  2. Encrypted-at-rest fallback – when no keyring backend is available (Linux headless acquisition machines per §7.4.4), the store writes a single Fernet-encrypted JSON document at <state_dir>/exlab-wizard/secrets.enc. The Fernet key is derived from a master passphrase via Argon2id (time_cost=3, memory_cost=64 MiB, parallelism=4). The salt lives next to the ciphertext in the same file (the salt is not secret; the passphrase is).

The store API is intentionally credential-agnostic: it indexes secrets by (KEYRING_SERVICE, username) so the LIMS credential (username="lims") and per-equipment NAS credentials (username="nas:<equipment_id>") share one store.

The passphrase_provider callable is invoked lazily on the first fallback-mode operation so that working keyring environments never prompt the operator. The launcher wires it to getpass.getpass per §7.4.4 step 3; tests can pass a constant-returning lambda.

Classes

KeyringStore(*, state_dir[, passphrase_provider])

OS keyring with encrypted-at-rest fallback.

class exlab_wizard.lims.keyring_store.KeyringStore(*, state_dir, passphrase_provider=None)[source]#

Bases: object

OS keyring with encrypted-at-rest fallback.

Backend Spec §7.4. Tries the OS keyring first via the keyring module. When that backend raises keyring.errors.KeyringError (or one of its subclasses for “no backend installed”), falls back to <state_dir>/exlab-wizard/secrets.enc encrypted with Fernet (AES-128-CBC + HMAC-SHA256), keyed by an Argon2id KDF over a user-supplied master passphrase.

The fallback path requires a passphrase_provider callable; if the keyring is available this argument may be None – it is only invoked when the fallback engages.

Parameters:
delete_password(*, username)[source]#

Remove the secret stored under (KEYRING_SERVICE, username).

Missing entries are silent in both backends. Keyring errors defer to the fallback.

Parameters:

username (str)

Return type:

None

get_password(*, username)[source]#

Look up the secret for (KEYRING_SERVICE, username).

Returns None when the entry is absent in both backends. Errors from the OS keyring are caught and the fallback is consulted; errors from the fallback propagate as exlab_wizard.errors.KeyringUnavailableError.

Parameters:

username (str)

Return type:

str | None

is_keyring_available()[source]#

Best-effort probe of the OS keyring backend.

Implemented as a round-trip set + delete of a sentinel value. A backend that raises on either side is considered unavailable. Per §7.4.4 the keyring package’s “fail” backends raise keyring.errors.KeyringError from these calls.

Return type:

bool

set_password(*, username, password)[source]#

Persist password under (KEYRING_SERVICE, username).

On a keyring backend error the fallback file is written; on fallback failure this raises exlab_wizard.errors.KeyringUnavailableError.

Parameters:
  • username (str)

  • password (str)

Return type:

None