# Shared contract — `ZB.MOM.WW.Secrets` The public API of the shared secrets library. Unlike most `shared-contract/` docs (which propose an API to extract), this component's library is **already built and published** — this records the **realized** surface (v0.1.2 on the `dohertj2-gitea` NuGet feed). Source lives in-repo at [`ZB.MOM.WW.Secrets/`](../../../ZB.MOM.WW.Secrets/). ## Packages (mirrors `ZB.MOM.WW.Auth`'s shape) | Package | Role | Depends on | |---|---|---| | `ZB.MOM.WW.Secrets.Abstractions` | Contracts only — interfaces, records, enums, exceptions | BCL + `DI.Abstractions` | | `ZB.MOM.WW.Secrets` | Impl — cipher, master-key providers, SQLite store + migrator, resolver, `${secret:}` expander, `AddZbSecrets` | Abstractions, `Microsoft.Data.Sqlite`, `ZB.MOM.WW.Audit` | | `ZB.MOM.WW.Secrets.Ui` | Blazor RCL on `ZB.MOM.WW.Theme` — `/admin/secrets` (list / add / rotate / delete / gated reveal) | Abstractions, `ZB.MOM.WW.Theme`, `Auth.AspNetCore` | | `ZB.MOM.WW.Secrets.Cli` *(tool, not packed)* | `secret set / get / list / rm / rotate / rewrap-all`; plus a menu-driven **interactive console** (bare `secret` on a TTY) for deployment seeding + KEK-lockout recovery — see the [runbook](../../../ZB.MOM.WW.Secrets/docs/operations/interactive-console.md) | Secrets | | `ZB.MOM.WW.Secrets.Akka` *(deferred — not built)* | Cluster replication of encrypted rows | Secrets, Akka.Cluster | ## Abstractions (contracts) Interfaces: `IMasterKeyProvider`, `ISecretCipher`, `ISecretStore`, `ISecretResolver`, `ISecretReplicator`, `ISecretCacheInvalidator`, `ISecretActorAccessor`. ```csharp public interface IMasterKeyProvider // 32-byte KEK; KekId identifies the active key { ReadOnlyMemory GetMasterKey(); string KekId { get; } } public interface ISecretStore // pluggable storage (SQLite default) { Task GetAsync(SecretName name, CancellationToken ct); Task UpsertAsync(StoredSecret row, CancellationToken ct); // overwrite-in-place Task DeleteAsync(SecretName name, CancellationToken ct); // soft delete (tombstone) Task> ListAsync(CancellationToken ct); // metadata only Task> GetManifestAsync(CancellationToken ct); // (name → revision) Task ApplyReplicatedAsync(StoredSecret row, CancellationToken ct); // LWW upsert (cluster) } public interface ISecretResolver // app-runtime read (cached, audited) { Task GetAsync(SecretName name, CancellationToken ct); } public interface ISecretCacheInvalidator { /* invalidate on rotate/delete */ } public interface ISecretReplicator { /* no-op default; Akka impl deferred */ } public interface ISecretActorAccessor { /* supplies the audit actor from the host */ } ``` Types: `SecretName` (validated `readonly record struct` — rejects `..` path-traversal, rooted/`//` segments, empty; fail-fast on `default`), `SecretContentType` enum (`Text` | `ConnectionString` | `Json` | `BinaryBase64`), `StoredSecret` (the ciphertext row), `SecretMetadata` (**no** `byte[]` members — safe to list/log), `SecretManifestEntry`. Exceptions (all fail-closed): `MasterKeyUnavailableException`, `SecretDecryptionException` (GCM tag mismatch / tamper / wrong KEK), `SecretNotFoundException`, `SecretStoreMigrationException`. ## Implementation (core) - **`AesGcmEnvelopeCipher`** (`…Crypto`) — per-write DEK, AES-256-GCM, AAD name-binding, DEK-wrap under the KEK, `CryptographicOperations.ZeroMemory` on DEK material. - **Master-key providers** (`…MasterKey`) — `Environment` / `File` / `Dpapi` selected by `MasterKeyProviderFactory` from `MasterKeyOptions`; 32-byte validated; fail-closed. - **`SqliteSecretStore`** + `SqliteSecretsStoreMigrator` + `SecretsSqliteConnectionFactory` (`…Sqlite`) — schema v1, WAL + `busy_timeout`, idempotent migration. - **`DefaultSecretResolver`** — TTL cache; audits every resolve (Success/Failure) via `ZB.MOM.WW.Audit`; **fail-loud** on decrypt failure (audit Failure, then rethrow). - **`SecretReferenceExpander`** (`…Configuration`) — `${secret:name}` expansion pre-host, fail-closed, skips `_`-prefixed comment keys. - **`AddZbSecrets(config, sectionPath)`** (`…DependencyInjection`) — one call wires the provider + cipher + store + resolver (also exposed as `ISecretCacheInvalidator`) + no-op replicator + a startup migration hosted service. Binds `SecretsOptions` (`SqlitePath`, `MasterKey`, `RunMigrationsOnStartup`, `ResolveCacheTtl`). ## UI + authz - `ZB.MOM.WW.Secrets.Ui` — `SecretsPage` (`@page "/admin/secrets"`) mounted via the host's `AddAdditionalAssemblies(SecretsPage.Assembly)`; `SecretsList`, `SecretEditor`, `ConfirmDeleteModal` (in-page Theme modal), `RevealButton`. - `SecretsAuthorization.AddSecretsAuthorization()` adds two named policies: **`secrets:manage`** (`ManageRole`/`RevealRole`/`Administrator`) for CRUD and **`secrets:reveal`** (`RevealRole`/`Administrator`) for plaintext reveal. Role names are compared case-sensitively (Ordinal) by ASP.NET — the admin role is `nameof(CanonicalRole.Administrator)` to match the family's canonical vocabulary. ## Configuration shape ```json "Secrets": { "SqlitePath": "app-secrets.db", "MasterKey": { "Source": "Environment", "EnvVarName": "ZB_SECRETS_MASTER_KEY" }, "RunMigrationsOnStartup": true, "ResolveCacheTtl": "00:00:30" } ``` `MasterKey.Source` ∈ `Environment` | `File` | `Dpapi`. The master key itself is **never** committed — env var (containers), mounted key file (clustered pairs), or DPAPI (Windows). ## Consumer wiring (reference: HistorianGateway) 1. Pre-host: a throwaway `ServiceCollection().AddZbSecrets(...)` provider → `SqliteSecretsStoreMigrator.MigrateAsync` → `SecretReferenceExpander.ExpandConfigurationAsync` over `builder.Configuration`, **before** options bind/validation (scoped `#pragma warning disable ASP0000` for the deliberate throwaway container). 2. Runtime: `builder.Services.AddZbSecrets(...)` for the app's `ISecretResolver`. 3. UI: `Configure(o => o.AddSecretsAuthorization())` + `AddAdditionalAssemblies(SecretsPage.Assembly)`. 4. A config value becomes `"${secret:sql//}"`; the value is seeded via the CLI or the `/admin/secrets` UI. **Live-proven** end-to-end 2026-07-16: a resolved `${secret:}` password authenticated a real read against the wonder historian (wrong secret → `HistorianAuthException`; correct → real samples).