Skip to content

Extract AtRestSecretProtector as the single at-rest secret contract#445

Merged
ppXD merged 2 commits into
mainfrom
fix/p2-atrest-secret-protector
Jun 15, 2026
Merged

Extract AtRestSecretProtector as the single at-rest secret contract#445
ppXD merged 2 commits into
mainfrom
fix/p2-atrest-secret-protector

Conversation

@ppXD

@ppXD ppXD commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator

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).

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 contract 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.

Pure refactor, behaviour-identical and non-breaking: the provider ctors swap
IVariableEncryptionService for IAtRestSecretProtector (both auto-registered via
IScopedDependency, DI-resolved everywhere — no call site constructs them by
hand). The existing real-Postgres at-rest integration tests for all three
providers pass unchanged, proving the round-trip behaviour is preserved.

Tests: AtRestSecretProtectorTests pins the idempotent-Protect (null/empty/
already-encrypted passthrough) + read-both Unprotect contract; the account/
feed/certificate at-rest integration tests are the behaviour-preservation proof.
Full unit suite (6043) green.
@ppXD ppXD added this to the 1.9.3 milestone Jun 15, 2026
… real-service tests

Two fix-optional review items, both folded in:

- Document that the Protect (sync) / UnprotectAsync (async) asymmetry is
  deliberate — it mirrors IVariableEncryptionService (EncryptAsync is
  synchronous despite its name; DecryptAsync is genuinely async) — so a future
  reader does not "normalize" the two halves.
- Add two real-VariableEncryptionService-backed cases to the protector unit
  file so the idempotency + V2-envelope contract is pinned at the seam itself
  (Protect(plaintext) -> envelope, re-Protect -> unchanged, round-trip), not
  only via the provider/Postgres integration tier.
@ppXD
ppXD merged commit 11051a2 into main Jun 15, 2026
15 checks passed
@ppXD
ppXD deleted the fix/p2-atrest-secret-protector branch June 15, 2026 01:28
@ppXD ppXD mentioned this pull request Jun 15, 2026
3 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant