Lockspire. Admin. Keys
(lockspire v1.6.1)
Copy Markdown
Operator-facing query and command boundary for guided signing-key lifecycle work.
Summary
Functions
Reports how far stored signing keys are from the current key-encryption key.
Converts stored signing-key material to the current key-encryption key.
Types
@type encryption_status() :: %{ configured: boolean(), current_kek_id: String.t() | nil, configured_kek_ids: [String.t()], legacy_plaintext: :allow | :reject, total: non_neg_integer(), no_private_key: non_neg_integer(), legacy: non_neg_integer(), by_kek_id: %{optional(String.t()) => non_neg_integer()}, unknown_kek: %{optional(String.t()) => non_neg_integer()}, undecryptable: non_neg_integer(), stranded_retired: %{count: non_neg_integer(), kids: [String.t()]}, pending: [ %{ kid: String.t(), state: :legacy | :unsealable_legacy | :other_kek | :unknown_kek | :undecryptable } ], ready?: boolean() }
@type key_view() :: %{ key: Lockspire.Domain.SigningKey.t() | map(), published: boolean(), publishable: boolean(), next_actions: [lifecycle_action()] }
@type lifecycle_action() :: :publish | :activate | :retire
@type reencrypt_report() :: %{ dry_run: boolean(), current_kek_id: String.t(), scanned: non_neg_integer(), encrypted: non_neg_integer(), rewrapped: non_neg_integer(), already_current: non_neg_integer(), no_private_key: non_neg_integer(), conflicted: non_neg_integer(), by_source_kek: %{optional(String.t()) => non_neg_integer()}, failed: [ %{ kid: String.t() | nil, reason: atom(), kek_id: String.t() | nil, status: Lockspire.Domain.SigningKey.status() | nil } ], changes: [ %{kid: String.t(), action: :encrypted | :rewrapped, from: String.t()} ] }
Functions
@spec encryption_status() :: {:ok, encryption_status()} | {:error, :repo_not_started | :storage_unavailable | {:invalid_config, String.t()}}
Reports how far stored signing keys are from the current key-encryption key.
Every stored private key is opened with a freshly resolved key set and the plaintext is
discarded, so a row sealed under the same id with different material counts as undecryptable.
The result never contains stored bytes or key material.
ready? is true when a key-encryption key is configured, no legacy row remains, every row
sits on the current id, and no unknown or undecryptable row remains. Retired keys that cannot
be re-encrypted are listed under stranded_retired and never block readiness. This function
reads only: it takes no locks, writes nothing and emits no telemetry.
@spec generate_key(Lockspire.Domain.SigningKey.use_type()) :: {:ok, key_view()} | {:error, :key_encryption_not_configured | term()}
@spec reencrypt(keyword()) :: {:ok, reencrypt_report()} | {:error, :key_encryption_not_configured | :repo_not_started | :invalid_option | {:invalid_config, String.t()}}
Converts stored signing-key material to the current key-encryption key.
Legacy JSON or term-encoded rows are sealed into the encrypted envelope, and rows sealed under another configured key are re-wrapped under the current one. Rows already on the current key are opened to prove they decrypt and are left untouched. Every status and both key uses are covered, and no lifecycle column changes.
Each row is converted in its own transaction, so a row that cannot be converted is listed
under failed in the report while the other rows stay committed. Running it again is safe:
rows already on the current key are counted as already_current.
Options: :dry_run (default false) classifies every row and writes nothing, and :actor
(a map with :type, :id and :display) is recorded on each audit entry. No other option
is supported; :page_size and :after_lock are internal test seams and may change.
The repo must already be started. On a running release node, call this through
bin/app rpc instead of a separate process.