Skip to content

Introduce Keyring: single deep module for envelope encryption#124

Merged
allisson merged 9 commits into
mainfrom
improve-codebase
May 23, 2026
Merged

Introduce Keyring: single deep module for envelope encryption#124
allisson merged 9 commits into
mainfrom
improve-codebase

Conversation

@allisson
Copy link
Copy Markdown
Owner

Summary

  • New module internal/keyring/ owns envelope encryption end-to-end (Encrypt/Decrypt + AllocateDek/EncryptWith/DecryptWith + Rewrap/RewrapAll). All three feature modules (secrets, transit, tokenization) and the KEK rotation CLI consume it instead of wiring KekChain + KeyManager + AEADManager + DekRepository themselves.
  • A deterministic keyring.Fake ships alongside, giving feature unit tests a real second adapter. Mock-driven unit tests rewritten against it — net +1,987 / −7,895 across the branch.
  • Decisions recorded in ADR-0013; ADR-0001 gains a Module-structure section pointing at it. Domain vocabulary captured in the new CONTEXT.md.

What changed where

Area Before After
secrets usecase constructor 8 deps 4 deps
tokenization key usecase 5 deps 3 deps
tokenization usecase 8 deps 5 deps
transit key usecase 6 deps 3 deps
internal/crypto/usecase/DekUseCase + DekRepository live deleted
Container.CryptoDekRepository/UseCase accessors live deleted

internal/crypto/{domain,service,repository} are unchanged — they're the primitives Keyring composes. internal/crypto/usecase/KekUseCase stays for the KEK lifecycle CLI (Create/Rotate/Unwrap).

Test plan

  • make test (race-enabled unit tests)
  • make test-all (unit + integration on Postgres) — confirmed green
  • make lint (golangci-lint + govulncheck) — 0 issues, 0 vulns
  • make mocks regenerated against updated .mockery.yaml
  • Smoke-check the CLI's rewrap-deks command once against a real DB after merge

🤖 Generated with Claude Code

allisson and others added 9 commits May 23, 2026 13:50
Adds internal/keyring as the single deep module for envelope encryption.
Exposes a six-method interface (Encrypt/Decrypt + AllocateDek/EncryptWith/
DecryptWith + Rewrap) backed by the existing crypto primitives, plus a
deterministic in-memory Fake for feature unit tests.

Features still use internal/crypto directly; subsequent commits migrate
secrets, tokenization, transit, and the rotation worker onto Keyring.

Also seeds CONTEXT.md with the domain vocabulary (Keyring, Envelope,
DekHandle, Rewrap, KEK, DEK).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The secret use case no longer needs kekChain, aeadManager, keyManager,
dekRepo, or dekAlgorithm. Constructor goes from 8 dependencies to 4:
txManager, keyring, secretRepo, sizeLimit.

The six-step envelope dance (CreateDek → persist → DecryptDek → CreateCipher
→ Encrypt → Zero) collapses to keyring.Encrypt. Decrypt collapses
symmetrically. The DekRepository interface in secrets/usecase is removed;
the keyring owns DEK lifecycle.

Unit tests are rewritten against keyring.Fake — the deterministic in-memory
adapter introduced with the keyring package. This is the seam in action:
two real adapters now (production keyring + Fake), not just mocks.

Net change: 1632→378 test lines, 249→156 production lines, no behaviour
change visible to HTTP callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both TokenizationKeyUseCase and TokenizationUseCase shed their crypto
plumbing. The "one DEK per tokenization key, reused across requests"
pattern maps cleanly onto Keyring's AllocateDek/EncryptWith/DecryptWith:

- TokenizationKeyUseCase: Create/Rotate call keyring.AllocateDek and
  persist the returned DekID on the tokenization key row.
- TokenizationUseCase: Tokenize and Detokenize build a DekHandle from
  the key's DekID and call EncryptWith/DecryptWith. No DEK lookup, no
  KEK lookup, no AEAD construction in the feature.

DekRepository interface dropped from tokenization/usecase. internal/crypto
no longer surfaces in tokenization. helpers.go::getKek removed — Keyring
owns KEK lookup internally.

Tests rewritten against keyring.Fake. Constructor parameters drop from
8 to 5 (key usecase: 5→3). Net change: 875+1855→192+304 test lines
(−82%), 245+395→204+282 production lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
TransitKeyUseCase sheds dekRepo, keyManager, aeadManager, and kekChain.
Constructor goes from 6 deps to 3: txManager, transitRepo, keyring.

Create/Rotate call keyring.AllocateDek and persist the returned DekID on
the transit key row. Encrypt/Decrypt build a DekHandle from the transit
key's DekID and call EncryptWith/DecryptWith. The transit-specific wire
format (version:base64(nonce||ciphertext) — ADR-0002) is preserved; the
12-byte nonce size is now a small constant in transit (both supported
algorithms use 12-byte nonces).

DekRepository interface removed from transit/usecase and transit/domain.
internal/crypto no longer surfaces in transit. Tests rewritten against
keyring.Fake.

Net: 6→3 constructor deps, ~1500 test lines removed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The rewrap-deks command now consumes a single Keyring instead of
separately wiring MasterKeyChain, KekUseCase, and DekUseCase. It calls
keyring.RewrapAll, which iterates batches internally via
dekStore.GetBatchNotKekID + Rewrap.

Adds two methods to Keyring:
- RewrapAll(ctx, batchSize) — batch rewrap to the active KEK
- ActiveKekID() — exposed so the worker can verify the operator-provided
  --kek-id matches what the boot-time chain knows. Prevents accidentally
  rewrapping against a stale chain after rotation.

Fake gains stubs for both. The dek_usecase.Rewrap path is no longer
called from any production code; it'll be deleted in the next commit
along with the rest of internal/crypto/usecase.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
internal/crypto/usecase shrinks to its remaining responsibility: KEK
lifecycle (Create, Rotate, Unwrap) driven by the rotation CLI. The DEK
envelope path now lives entirely in internal/keyring.

Removed:
- internal/crypto/usecase/dek_usecase.go + test
- DekUseCase and DekRepository interfaces
- Container.CryptoDekRepository, Container.CryptoDekUseCase accessors
- corresponding sync.Once fields, struct fields, and DI test cases
- mockery entries for the deleted interfaces

internal/crypto/{domain,service,repository} are unchanged — they hold
the cryptographic primitives Keyring composes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ADR-0013 records why envelope encryption became a single module rather
than a pattern enforced by convention: it gives ADR-0001's hierarchy a
home, eliminates the six-step dance from feature usecases, and turns
test mocks into a real second adapter (keyring.Fake).

ADR-0001 gains a "Module structure" section and a See-also link so
future architecture reviews don't re-suggest splitting the envelope
back out.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drops the orphan MockDekRepository / MockDekUseCase types from
internal/{crypto,secrets,tokenization,transit}/usecase/mocks/.
Generated by mockery v3.7.0 from the updated .mockery.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Satisfies markdownlint MD040 so `make docs-lint` passes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@allisson allisson merged commit 8e2a673 into main May 23, 2026
3 checks passed
@allisson allisson deleted the improve-codebase branch May 23, 2026 17:41
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