Skip to content

Extract AtRestSecretProtector as the single at-rest secret contract

Placeholder ppxd requested to merge fix/p2-atrest-secret-protector into main

Summary

  • The account / feed / certificate DataProviders each carried a near-identical value transform: encrypt-if-needed (guarded by IsValidEncryptedValue so a re-save never double-wraps) on write, and read-both decrypt (legacy plaintext passes through verbatim) on read — four copies of the same contract, and the upcoming SSH proxy-password slice would have been a fifth.
  • Extract it into IAtRestSecretProtector (Protect / UnprotectAsync over a KDF scope), wrapping IVariableEncryptionService. The three providers now depend on this narrow SSOT instead of re-implementing it.
  • The entity-level anti-flush concern (QueryNoTracking, or Detach before the in-place decrypt) stays in each provider — it is entity-specific and orthogonal to the value transform.

Foundation for the remaining P2 at-rest finishers (SSH proxy password, then Tentacle private key).

Non-breaking

Pure refactor, behaviour-identical: the provider ctors swap IVariableEncryptionService for IAtRestSecretProtector (both auto-registered via IScopedDependency, DI-resolved everywhere — no call site constructs them by hand). No interface/enum/signature/wire/DB change.

Test plan

  • Unit — AtRestSecretProtectorTests: idempotent Protect (null / empty / already-encrypted passthrough, no double-wrap) + read-both UnprotectAsync (delegates to the encryption service, legacy plaintext passthrough) + KDF scope forwarded.
  • Integration (real Postgres + real DI) — the existing account / feed / certificate at-rest round-trip tests pass unchanged through the now-DI-resolved real protector (12/12), proving behaviour is preserved.
  • Full unit suite green (6043).

Merge request reports

Loading