Introduce Keyring: single deep module for envelope encryption#124
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
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.keyring.Fakeships alongside, giving feature unit tests a real second adapter. Mock-driven unit tests rewritten against it — net +1,987 / −7,895 across the branch.CONTEXT.md.What changed where
secretsusecase constructortokenizationkey usecasetokenizationusecasetransitkey usecaseinternal/crypto/usecase/DekUseCase+DekRepositoryContainer.CryptoDekRepository/UseCaseaccessorsinternal/crypto/{domain,service,repository}are unchanged — they're the primitives Keyring composes.internal/crypto/usecase/KekUseCasestays for the KEK lifecycle CLI (Create/Rotate/Unwrap).Test plan
make test(race-enabled unit tests)make test-all(unit + integration on Postgres) — confirmed greenmake lint(golangci-lint + govulncheck) — 0 issues, 0 vulnsmake mocksregenerated against updated.mockery.yamlrewrap-dekscommand once against a real DB after merge🤖 Generated with Claude Code