From 16b32257f50b20483e5c8c4ebfe4a00a89218c79 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Wed, 15 Jul 2026 16:30:19 -0400 Subject: [PATCH] feat(secrets): abstractions seam interfaces --- .../IMasterKeyProvider.cs | 24 +++++++ .../ISecretCipher.cs | 35 ++++++++++ .../ISecretReplicator.cs | 17 +++++ .../ISecretResolver.cs | 19 ++++++ .../ISecretStore.cs | 66 +++++++++++++++++++ 5 files changed, 161 insertions(+) create mode 100644 ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/IMasterKeyProvider.cs create mode 100644 ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretCipher.cs create mode 100644 ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretReplicator.cs create mode 100644 ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretResolver.cs create mode 100644 ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretStore.cs diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/IMasterKeyProvider.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/IMasterKeyProvider.cs new file mode 100644 index 0000000..dd2eb5c --- /dev/null +++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/IMasterKeyProvider.cs @@ -0,0 +1,24 @@ +namespace ZB.MOM.WW.Secrets.Abstractions; + +/// +/// Supplies the AES-256 master key (KEK) used to wrap and unwrap per-secret data-encryption +/// keys. Implementations resolve the key material from a configured source (environment, +/// file, external KMS) and expose a stable, non-secret identifier for the active key so +/// rotation can be recorded per row and reasoned about later. +/// +public interface IMasterKeyProvider +{ + /// + /// Returns the 32-byte AES-256 master key material. + /// + /// The 32-byte master key. + /// The master key cannot be resolved. + ReadOnlyMemory GetMasterKey(); + + /// + /// The stable, non-secret identifier of the active key-encryption key (KEK). This value + /// is recorded on each stored row so that key rotation is legible and rows can be matched + /// to the KEK that wrapped their DEK. + /// + string KekId { get; } +} diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretCipher.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretCipher.cs new file mode 100644 index 0000000..6fe9876 --- /dev/null +++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretCipher.cs @@ -0,0 +1,35 @@ +namespace ZB.MOM.WW.Secrets.Abstractions; + +/// +/// Envelope-encrypts and decrypts secret material. Each secret is sealed under a per-secret +/// data-encryption key (DEK) that is itself wrapped by the master key (KEK) from an +/// . The secret name is bound as additional authenticated +/// data (AAD) so ciphertext cannot be silently relocated to a different name. +/// +public interface ISecretCipher +{ + /// + /// Envelope-encrypts , binding it to + /// as additional authenticated data. + /// + /// The secret name, bound as AAD. + /// The secret plaintext to seal. + /// How the plaintext should be interpreted by consumers. + /// + /// A carrying the ciphertext, nonce, tag, and wrapped DEK, with + /// 0 and default timestamps — the store stamps the + /// revision and timestamps on persist. + /// + StoredSecret Encrypt(SecretName name, string plaintext, SecretContentType contentType); + + /// + /// Decrypts the value carried by a stored row, verifying its authentication tag and the + /// name it was bound to. + /// + /// The stored row to decrypt. + /// The decrypted plaintext. + /// + /// The authentication tag fails to verify, or the row references an unknown or unavailable KEK. + /// + string Decrypt(StoredSecret secret); +} diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretReplicator.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretReplicator.cs new file mode 100644 index 0000000..a11dce8 --- /dev/null +++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretReplicator.cs @@ -0,0 +1,17 @@ +namespace ZB.MOM.WW.Secrets.Abstractions; + +/// +/// Publishes an encrypted row to cluster peers. Core registers a no-op implementation; +/// ZB.MOM.WW.Secrets.Akka (deferred) provides the real cluster implementation. +/// +public interface ISecretReplicator +{ + /// + /// Broadcasts the already-encrypted to peer nodes. This is a no-op + /// in single-node deployments. + /// + /// The encrypted row to replicate. + /// A token to cancel the operation. + /// A task that completes when the row has been published. + Task PublishAsync(StoredSecret row, CancellationToken ct); +} diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretResolver.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretResolver.cs new file mode 100644 index 0000000..2335f37 --- /dev/null +++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretResolver.cs @@ -0,0 +1,19 @@ +namespace ZB.MOM.WW.Secrets.Abstractions; + +/// +/// The consumer-facing read seam: resolves the current plaintext of a secret by name. This is +/// the surface application code depends on to fetch secrets at runtime; every resolution is +/// audited (the access, never the value). +/// +public interface ISecretResolver +{ + /// + /// Resolves and decrypts the current plaintext for . + /// + /// The secret to resolve. + /// A token to cancel the operation. + /// + /// The decrypted plaintext, or null if the secret is absent or tombstoned. + /// + Task GetAsync(SecretName name, CancellationToken ct); +} diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretStore.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretStore.cs new file mode 100644 index 0000000..2662f3a --- /dev/null +++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets.Abstractions/ISecretStore.cs @@ -0,0 +1,66 @@ +namespace ZB.MOM.WW.Secrets.Abstractions; + +/// +/// Persists encrypted secret rows and exposes the read, list, and replication operations the +/// core services build on. The store only ever handles the encrypted +/// representation and its safe projection — it never sees plaintext. +/// Deletes are soft (tombstones) so removals can propagate across a cluster. +/// +public interface ISecretStore +{ + /// + /// Returns the row for , including a tombstoned row, or null + /// if no row exists. + /// + /// The secret to fetch. + /// A token to cancel the operation. + /// The stored row, or null if absent. + Task GetAsync(SecretName name, CancellationToken ct); + + /// + /// Inserts the row, or overwrites an existing row in place, bumping its revision and + /// updated timestamp. + /// + /// The encrypted row to persist. + /// A token to cancel the operation. + /// A task that completes when the row is persisted. + Task UpsertAsync(StoredSecret row, CancellationToken ct); + + /// + /// Soft-deletes (tombstones) the row for so the deletion can + /// propagate to peers. + /// + /// The secret to tombstone. + /// The principal to record as updated_by, if known. + /// A token to cancel the operation. + /// true if a row was tombstoned; false if the row was absent. + Task DeleteAsync(SecretName name, string? actor, CancellationToken ct); + + /// + /// Lists the safe metadata projection for all secrets — never ciphertext. + /// + /// + /// When true, tombstoned rows are included; otherwise they are omitted. + /// + /// A token to cancel the operation. + /// The metadata for the matching secrets. + Task> ListAsync(bool includeDeleted, CancellationToken ct); + + /// + /// Returns a compact manifest — name, revision, updated timestamp, and tombstone flag per + /// secret — used for cluster anti-entropy reconciliation without exchanging ciphertext. + /// + /// A token to cancel the operation. + /// The manifest entries for all secrets. + Task> GetManifestAsync(CancellationToken ct); + + /// + /// Applies a row received from a peer using last-writer-wins on + /// (, ). A row that + /// is not newer than the local row is ignored. + /// + /// The replicated encrypted row to apply. + /// A token to cancel the operation. + /// A task that completes when the row has been applied or ignored. + Task ApplyReplicatedAsync(StoredSecret row, CancellationToken ct); +}