Conversation
Implements the foundation layer of the data-at-rest encryption design (docs/design/2026_04_29_proposed_data_at_rest_encryption.md §4.1). New internal/encryption/ package: - cipher.go: AES-256-GCM Encrypt/Decrypt primitive over a Keystore. Caller-supplied AAD bytes (no AAD composition baked in -- storage and raft layers compose their own AAD per §4.1 / §4.2 in later stages). Decrypt wraps GCM tag mismatch as ErrIntegrity with the original Open error attached via WithSecondaryError so callers match via errors.Is and never silently zero/retry. - envelope.go: §4.1 wire format encoder/decoder. Layout: [version 1B][flag 1B][key_id 4B BE][nonce 12B][ciphertext+tag]. EnvelopeOverhead = 34 bytes. HeaderAADBytes helper exposed for callers that compose storage AAD = HeaderAADBytes || pebble_key. ReservedKeyID = 0 sentinel per §5.1. Constants HeaderAADSize / HeaderSize / TagSize / NonceSize / KeySize. - keystore.go: copy-on-write atomic.Pointer[map[uint32][]byte] DEK store. Get is a single atomic load (no mutex on the hot path per §10 self-review lens 2). Set/Delete CAS in a new map. Set rejects ReservedKeyID and non-32-byte DEKs. Set copies the input so callers may zero or reuse the source slice. - errors.go: typed errors -- ErrUnknownKeyID, ErrReservedKeyID, ErrBadNonceSize, ErrBadKeySize, ErrIntegrity, ErrEnvelopeShort, ErrEnvelopeVersion. ErrIntegrity is the load-bearing error for §4.1 -- callers MUST treat it as a typed read error. - kek/kek.go: Wrapper interface (Wrap/Unwrap/Name). Documents that Stage 0 ships only the file-based provider; KMS providers (aws_kms, gcp_kms, vault) come in Stage 9 per §5.1. - kek/file.go: FileWrapper using AES-256-GCM under a 32-byte KEK read from a file. Output: [12B nonce][32B ct][16B tag] = 60 bytes. Validates KEK file length on load and DEK length on Wrap; rejects bad lengths instead of padding/truncating. Tests: - cipher_test.go: round-trip across plaintext/AAD shapes; tag/AAD/ciphertext/nonce tamper all rejected with ErrIntegrity; reserved key_id, bad nonce size, unknown key_id all return typed errors; distinct nonces produce distinct ciphertexts. - cipher_prop_test.go: pgregory.net/rapid property tests -- arbitrary plaintext/AAD round-trips, single-bit AAD flip rejected (the §4.1 cut-and-paste defence property). - envelope_test.go: encode/decode round-trip across body sizes and key_id values; rejects under-length input (ErrEnvelopeShort); rejects unknown version bytes (ErrEnvelopeVersion); decode does not alias input. - keystore_test.go: Set/Get; reserved/bad-size rejection; Set copies input; Delete; IDs sorted; concurrent reader/writer stress under -race. - kek/file_test.go: round-trip; distinct nonces between Wrap calls; bad KEK / DEK / wrapped lengths; tag/nonce tamper rejected; missing-file error. No integration with existing storage / raft / FSM layers -- pure library + tests. Stage 1 (sidecar) and Stage 2 (storage hooks) follow per the staging plan in PR #707. Self-review: 1. Data loss: pure library, no persistence yet. ErrIntegrity is propagated, never swallowed. Round-trip property test guards envelope-format bugs. 2. Concurrency: atomic.Pointer[map] for the keystore -- writers CAS, readers atomic-load. Concurrent reader/writer stress test runs under -race. 3. Performance: AES-NI fast path via crypto/aes; no allocations beyond the per-call ciphertext buffer (sync.Pool optimisation deferred to integration stages where the hot path is real). 4. Data consistency: no MVCC/OCC interaction yet. Foundation primitive only. 5. Test coverage: 23 tests + 2 property tests; -race clean; 0 lint issues against the project .golangci.yaml.
|
Warning Rate limit exceeded
To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Review rate limit: 0/1 reviews remaining, refill in 33 minutes and 37 seconds.Comment |
|
@claude review |
|
Claude finished @bootjp's task in 3m 1s —— View job Code Review:
|
There was a problem hiding this comment.
Code Review
This pull request implements the foundation for AES-256-GCM envelope encryption, including a thread-safe copy-on-write Keystore for Data Encryption Keys (DEKs), a Cipher for cryptographic operations, and wire format encoding/decoding. The review feedback focuses on performance optimizations for the hot path, specifically recommending the caching of cipher.AEAD instances to avoid repeated initialization overhead and suggesting an alternative API for AAD byte generation to reduce GC pressure.
| block, err := aes.NewCipher(dek) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "encryption: aes.NewCipher") | ||
| } | ||
| aead, err := cipher.NewGCM(block) | ||
| if err != nil { | ||
| return nil, errors.Wrap(err, "encryption: cipher.NewGCM") | ||
| } | ||
| return aead, nil |
There was a problem hiding this comment.
The aeadFor method performs AES key expansion (aes.NewCipher) and GCM initialization (cipher.NewGCM) on every encryption and decryption operation. These are computationally expensive operations that will significantly degrade performance on the hot path. Since Data Encryption Keys (DEKs) are relatively stable, the cipher.AEAD instances should be pre-computed and cached (e.g., within the Keystore) to avoid this overhead.
References
- Optimizing the hot path by caching expensive cryptographic initializations aligns with repository performance standards for frequently accessed code paths.
| // Keystore is a copy-on-write map from key_id to DEK bytes. | ||
| // | ||
| // Reads on the hot path (Cipher.Encrypt / Cipher.Decrypt) take a single | ||
| // atomic load and observe an immutable snapshot of the DEK map. Writes | ||
| // (rotation, bootstrap, retire) allocate a new map and CAS it in via | ||
| // atomic.Pointer.Store. | ||
| // | ||
| // Per §10 self-review lens 2: this avoids contending a mutex on the hot | ||
| // path while keeping rotation atomic with respect to readers. | ||
| type Keystore struct { | ||
| snap atomic.Pointer[map[uint32][]byte] | ||
| } |
There was a problem hiding this comment.
To improve performance on the hot path (encryption/decryption), the Keystore should store pre-initialized cipher.AEAD instances instead of just raw DEK bytes. This avoids repeated key expansion and GCM setup costs. Consider changing the internal map to store a struct containing both the raw DEK and the initialized cipher.AEAD object.
References
- Pre-computing and caching AEAD instances avoids repeated overhead on hot paths, consistent with repository performance optimization guidelines.
| func HeaderAADBytes(version, flag byte, keyID uint32) []byte { | ||
| out := make([]byte, HeaderAADSize) | ||
| out[0] = version | ||
| out[1] = flag | ||
| binary.BigEndian.PutUint32(out[2:HeaderAADSize], keyID) | ||
| return out | ||
| } |
There was a problem hiding this comment.
This function allocates a new byte slice on every call, which can lead to significant GC pressure in high-frequency operations. Consider an alternative API that allows the caller to pass a pre-allocated buffer or appends to an existing slice. When doing so, ensure a strictly enforced maximum size limit is checked to prevent unbounded memory growth, consistent with repository performance and safety rules.
| func HeaderAADBytes(version, flag byte, keyID uint32) []byte { | |
| out := make([]byte, HeaderAADSize) | |
| out[0] = version | |
| out[1] = flag | |
| binary.BigEndian.PutUint32(out[2:HeaderAADSize], keyID) | |
| return out | |
| } | |
| func AppendHeaderAADBytes(dst []byte, version, flag byte, keyID uint32) []byte { | |
| out := [HeaderAADSize]byte{ | |
| version, | |
| flag, | |
| } | |
| binary.BigEndian.PutUint32(out[2:], keyID) | |
| return append(dst, out[:]...) | |
| } |
References
- Pre-allocating buffers is acceptable and preferred for performance when strictly enforced maximum size limits are checked before allocation to prevent OOM issues.
|
@claude review |
|
Claude finished @bootjp's task in 2m 38s —— View job Code Review:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 12f6dfc9e0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| func NewCipher(ks *Keystore) *Cipher { | ||
| return &Cipher{keystore: ks} | ||
| } |
There was a problem hiding this comment.
Reject nil keystore when constructing a Cipher
NewCipher currently accepts a nil *Keystore, but the first Encrypt/Decrypt call will panic when aeadFor dereferences c.keystore. This turns a configuration/setup mistake into a process crash instead of a typed error, which is risky for production startup and rotation paths where dependencies may be wired dynamically.
Useful? React with 👍 / 👎.
| m := *k.snap.Load() | ||
| dek, ok := m[keyID] | ||
| return dek, ok |
There was a problem hiding this comment.
Make zero-value Keystore safe before dereferencing snap
All accessors dereference *k.snap.Load() without checking whether snap was initialized, so a zero-value Keystore (var ks Keystore or embedded struct) panics on first use. Because Keystore is exported, this is an easy misuse pattern and can crash callers outside tests; initializing lazily or guarding nil would avoid that failure mode.
Useful? React with 👍 / 👎.
…e[bot] Must Fix x3 + Should/Minor) gemini-code-assist High #1+#2: Pre-cache cipher.AEAD inside Keystore. AES key expansion + GCM init was happening on every Encrypt/Decrypt -- a measurable hot-path regression. Keystore internal map switches from map[uint32][]byte to map[uint32]*keyEntry holding both [KeySize]byte dek and the pre-init cipher.AEAD. New AEAD(keyID) (cipher.AEAD, bool) lookup runs on the hot path; aes.NewCipher / cipher.NewGCM happen once at Set time. claude[bot] Must Fix #2: Keystore.Get returned a slice into the live map; a caller violating the "MUST NOT mutate" contract would corrupt the active DEK silently. Replaced Get with DEK(keyID) ([KeySize]byte, bool) returning a value copy by virtue of the array type. The signature itself enforces immutability of the live entry. claude[bot] Must Fix #1: Cipher.Encrypt doc gains a CRITICAL nonce-reuse warning. Under AES-GCM, reusing (key, nonce) leaks plaintext XOR and enables authentication-key recovery; the §4.1 deterministic node_id || local_epoch || write_count construction is the supported uniqueness scheme. claude[bot] Must Fix #3: Added TestCipher_Decrypt_RejectsReservedKeyID and TestCipher_Decrypt_RejectsBadNonceSize so both Encrypt and Decrypt entry points have explicit coverage of ReservedKeyID and bad-nonce-size rejection. claude[bot] Should Fix #4: EnvelopeVersionV1 doc clarified -- the §11.3 reserved range 0x02..0x0F is for FUTURE decoders; the current build accepts only 0x01 and rejects everything else (including the reserved range) with ErrEnvelopeVersion. claude[bot] Should Fix #5: Envelope.Encode() now validates Version and Body length and returns (bytes, error). Bad inputs fail fast at build time (ErrEnvelopeVersion / ErrEnvelopeShort) instead of producing undecodable bytes that surface later as a confusing read-side failure. gemini-code-assist Medium #3: New AppendHeaderAADBytes(dst, version, flag, key_id) lets storage callers compose AAD onto a pooled buffer without allocating. HeaderAADBytes is now a thin wrapper over AppendHeaderAADBytes. claude[bot] Minor #6: sort.Slice -> slices.Sort in Keystore.IDs (Go 1.25 idiomatic, no closure allocation). claude[bot] Minor #7: FileWrapper.Name() now returns "file:<path>" so log lines and the EncryptionAdmin status RPC can distinguish multiple configured KEK files. claude[bot] Minor #8: Removed unreachable post-Open length check in FileWrapper.Unwrap; the input-length check above already guarantees Open returns exactly fileKEKSize bytes on success. Replaced with an explanatory comment. Tests: 28 unit + 2 property tests, -race clean, 0 lint issues against the project .golangci.yaml.
|
@claude review |
|
Claude finished @bootjp's task in 2m 34s —— View job Code Review:
|
codex P2 (line 26): NewCipher(*Keystore) accepted nil and the first Encrypt/Decrypt would nil-deref panic in aeadFor. Turns a setup mistake into a process crash on the dynamic dependency-wiring paths (sidecar resync §5.5, rotation §5.2). NewCipher signature changes from "(ks *Keystore) *Cipher" to "(ks *Keystore) (*Cipher, error)"; returns ErrNilKeystore on nil input. New ErrNilKeystore added to errors.go. Tests updated to use a mustCipher(t, ks) helper for the *testing.T paths and inline error handling for the *rapid.T property tests. New TestNewCipher_RejectsNil exercises the typed-error path. (gemini-code-assist Medium #3 line 156 -- HeaderAADBytes allocation -- already addressed in round-1 commit 5bd16c5 by adding AppendHeaderAADBytes; the comment is a re-anchor of the prior round.)
|
@claude review |
|
Claude finished @bootjp's task in 2m 39s —— View job Code Review:
|
… + Minor) claude[bot] r3 #1 Must Fix: keystore.go contained review-provenance comments ("gemini-code-assist review on PR #719" / "flagged by claude[bot] review") that violate CLAUDE.md (dont reference the current task or callers in code comments). Removed the parentheticals while keeping the WHY (hot-path performance + type-system immutability rationale). claude[bot] r3 #2 Should Fix: zero-value Keystore panics on first method call because atomic.Pointer starts nil. Added a "zero value is NOT safe to use" doc note on the Keystore type explaining that NewKeystore is mandatory and that struct embedding requires explicit initialization. claude[bot] r3 #4 Should Fix: added benchmark coverage for the post-redesign hot path. cipher_bench_test.go covers Encrypt/Decrypt across 64B / 1KiB / 16KiB / 64KiB plaintexts plus a Keystore.AEAD lookup microbenchmark. Quick run on Apple M1 Max: Encrypt @ 64KiB 3.4 GB/s, Keystore.AEAD 2.5 ns/op 0 allocs -- proves AES-NI engagement and that AEAD lookup is essentially free, validating the round-1 redesign. claude[bot] r3 #5 Minor: kek/file.go Wrap call now has an explicit comment that nil AAD is intentional (key-ID binding deferred to Stage 9 KMS providers). Prevents a future maintainer from silently breaking persisted wrapped-DEK blobs by adding AAD here in isolation. claude[bot] r3 #3 Should Fix (NewCipher nil check) was already addressed in round-2 commit cafe467 -- claude[bot] reviewed the prior commit.
|
@claude review |
|
Claude finished @bootjp's task in 2m 25s —— View job Code Review:
|
…(PR719 r4 #5) claude[bot] r4 #5: cipher_test.go:14 referenced a Keystore.Get method that was removed in round-1 (replaced by DEK / AEAD split). Update the stale comment to point at DEK(testKeyID). claude[bot] r4 #1-#4 (review-provenance comments / zero-value Keystore note / benchmarks / FileWrapper nil AAD comment) were already addressed in round-3 commit 7b81812 -- claude[bot] r4 reviewed the prior commit cafe467 in parallel.
|
@claude review |
|
Claude finished @bootjp's task in 1m 9s —— View job Code Review:
|
claude[bot] r6: cipher_bench_test.go formatBytes had an O(n^2) prepend loop (append([]byte{d}, digits...) reallocates per iteration). The justification "avoid fmt.Sprintf to keep the file self-contained" did not hold -- strconv is already in the Go stdlib with zero transitive imports. Replaced the helper with strconv.Itoa inlined into name(), deleted formatBytes entirely.
|
@claude review |
|
Claude finished @bootjp's task in 2m 34s —— View job Code Review:
|
| Round | Item | Status |
|---|---|---|
| R1 | Nonce-reuse warning in Encrypt doc |
✅ |
| R1 | Keystore.Get mutable slice → DEK() [KeySize]byte value copy |
✅ |
| R1 | TestCipher_Decrypt_RejectsReservedKeyID / RejectsBadNonceSize |
✅ |
| R1 | EnvelopeVersionV1 comment clarified |
✅ |
| R1 | Encode() validates and returns ([]byte, error) |
✅ |
| R1 | sort.Slice → slices.Sort |
✅ |
| R1 | FileWrapper.Name() returns "file:"+path |
✅ |
| R1 | Unreachable post-Open length check removed | ✅ |
| R1 | nil AAD in Wrap documented (Stage 9 deferral) |
✅ |
| R3 | Review-provenance parentheticals removed | ✅ |
| R3 | Zero-value safety warning on Keystore type |
✅ |
| R3 | NewCipher(nil) returns ErrNilKeystore |
✅ |
| R3 | Benchmarks added (Encrypt, Decrypt, Keystore_AEAD) |
✅ |
| R4 | Stale ks.Get(testKeyID) → ks.DEK(testKeyID) comment |
✅ |
| R5/R6 | formatBytes O(n²) loop → strconv.Itoa |
✅ |
Reviewer Checklist
CipherAPI does not bake storage/raft AAD — Confirmed.Encrypt/Decryptaccept rawaad []byte; §4.1 and §4.2 AAD composition is entirely the caller's responsibility.cipher.go:9-14documents this explicitly. ✅ReservedKeyID = 0rejected at all entry points — Confirmed in code (aeadForgates bothEncryptandDecryptatcipher.go:96,Keystore.Setatkeystore.go:94) and in tests (TestCipher_Encrypt_RejectsReservedKeyID,TestCipher_Decrypt_RejectsReservedKeyID,TestKeystore_Set_RejectsReservedKeyID). ✅
The package is correct, the architecture is sound, the keystore redesign (pre-initialized keyEntry.aead, [KeySize]byte immutability) is the right long-term shape, and the test and benchmark coverage is thorough. Ready to merge.
claude[bot] r7: the prior wording "do not bypass it with crypto/rand" could be misread as advice against using crypto/rand for nonces -- but crypto/rand is perfectly safe in tests / benchmarks. Reframe the warning to scope it to the §4.1 storage-layer integration: do not substitute a different nonce scheme there without a uniqueness proof. Add an explicit aside that fresh crypto/rand nonces are safe outside that integration. No code change.
|
@claude review |
|
Claude finished @bootjp's task in 2m 18s —— View job Code Review:
|
| Round | Item | Status |
|---|---|---|
| R1 | Nonce-reuse warning in Encrypt doc |
✅ |
| R1 | Keystore.Get mutable slice → DEK() [KeySize]byte value copy |
✅ |
| R1 | TestCipher_Decrypt_RejectsReservedKeyID / RejectsBadNonceSize |
✅ |
| R1 | EnvelopeVersionV1 comment clarified |
✅ |
| R1 | Encode() validates and returns ([]byte, error) |
✅ |
| R1 | sort.Slice → slices.Sort |
✅ |
| R1 | FileWrapper.Name() returns "file:"+path |
✅ |
| R1 | Unreachable post-Open length check removed | ✅ |
| R1 | nil AAD in Wrap documented (Stage 9 deferral) |
✅ |
| R3 | Review-provenance parentheticals removed from keystore.go |
✅ |
| R3 | Zero-value safety warning on Keystore type |
✅ |
| R3 | NewCipher(nil) returns ErrNilKeystore |
✅ |
| R3 | Benchmarks added (Encrypt, Decrypt, Keystore_AEAD) |
✅ |
| R4 | Stale ks.Get(testKeyID) → ks.DEK(testKeyID) comment |
✅ |
| R5/R6 | formatBytes O(n²) loop → strconv.Itoa |
✅ |
| R7 | Nonce doc clarified: crypto/rand is safe for tests/benchmarks | ✅ |
Reviewer Checklist
CipherAPI does not bake storage/raft AAD — Confirmed.Encrypt/Decryptaccept rawaad []byte;cipher.go:9-14documents the §4.1 / §4.2 orthogonality contract explicitly. ✅ReservedKeyID = 0rejected at all entry points — Confirmed in code (aeadForgates bothEncryptandDecryptatcipher.go:98,Keystore.Setatkeystore.go:94) and in tests (TestCipher_Encrypt_RejectsReservedKeyID,TestCipher_Decrypt_RejectsReservedKeyID,TestKeystore_Set_RejectsReservedKeyID). ✅
The package is functionally correct, the architecture is sound, the keystore redesign (pre-initialized keyEntry.aead, [KeySize]byte immutability, copy-on-write via atomic.Pointer) is the right long-term shape, and the test + benchmark coverage is thorough. Ready to merge.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c3f41a4d60
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if len(nonce) != NonceSize { | ||
| return nil, errors.Wrapf(ErrBadNonceSize, "got %d bytes, want %d", len(nonce), NonceSize) | ||
| } | ||
| aead, ok := c.keystore.AEAD(keyID) |
There was a problem hiding this comment.
Reject uninitialized Cipher before using keystore
Cipher is exported, so callers can create a zero-value instance (var c encryption.Cipher) without going through NewCipher; in that case c.keystore is nil and Encrypt/Decrypt panic when aeadFor calls c.keystore.AEAD. This turns a construction mistake into a runtime crash in any package that uses this internal API directly, so aeadFor should defensively return a typed error when c or c.keystore is nil.
Useful? React with 👍 / 👎.
Summary
Stage 0 of the data-at-rest encryption rollout from PR #707 (
docs/design/2026_04_29_proposed_data_at_rest_encryption.md). This PR is library-only — no integration with existing storage / raft / FSM layers. Those land in Stages 1–9.New package
internal/encryption/:cipher.go— AES-256-GCMEncrypt/Decryptprimitive over aKeystore. Caller-supplied AAD bytes (no AAD composition baked in — storage and raft layers compose their own AAD per §4.1 / §4.2 in later stages).Decryptwraps GCM tag mismatch asErrIntegritywith the originalOpenerror attached as a secondary cause, so callers match viaerrors.Isand never silently zero or retry.envelope.go— §4.1 wire format encoder/decoder. Layout:[version 1B][flag 1B][key_id 4B BE][nonce 12B][ciphertext+tag].EnvelopeOverhead = 34bytes.HeaderAADByteshelper exposed for storage callers that composeAAD = HeaderAADBytes ‖ pebble_key.ReservedKeyID = 0sentinel per §5.1.keystore.go— copy-on-writeatomic.Pointer[map[uint32][]byte]DEK store.Getis a single atomic load (no mutex on the hot path per §10 self-review lens 2).SetrejectsReservedKeyIDand non-32-byte DEKs; copies input.errors.go— typed errors:ErrUnknownKeyID,ErrReservedKeyID,ErrBadNonceSize,ErrBadKeySize,ErrIntegrity,ErrEnvelopeShort,ErrEnvelopeVersion.kek/kek.go—Wrapperinterface (Wrap/Unwrap/Name). KMS-backed providers (AWS KMS, GCP KMS, Vault) come in Stage 9.kek/file.go—FileWrapper: AES-256-GCM under a 32-byte KEK read from a file. Output[12B nonce][32B ct][16B tag] = 60 bytes. Validates lengths instead of padding/truncating.Tests:
cipher_test.go— round-trip across plaintext/AAD shapes; tag/AAD/ciphertext/nonce tamper rejected withErrIntegrity; typed error checks for reserved/bad-nonce/unknown-key cases; distinct nonces produce distinct ciphertexts.cipher_prop_test.go—pgregory.net/rapidproperty tests for arbitrary plaintext/AAD round-trip and single-bit AAD-flip rejection (the §4.1 cut-and-paste defence property).envelope_test.go— encode/decode round-trip; under-length input rejected; unknown version bytes rejected; decode does not alias input.keystore_test.go— Set/Get/Delete/IDs/Len; concurrent reader/writer stress under-race.kek/file_test.go— round-trip; distinct Wrap nonces; bad-length rejection; tag/nonce tamper rejected; missing file.Self-review (per CLAUDE.md 5 lenses)
ErrIntegrityalways propagated. Round-trip property test guards envelope-format bugs.atomic.Pointer[map]for keystore; concurrent stress test under-race.crypto/aes. No allocations beyond per-call ciphertext buffer (sync.Pool optimisation deferred to integration stages).-raceclean, 0 lint issues against project.golangci.yaml.Test plan
go test ./internal/encryption/...(~14s)go test -race ./internal/encryption/...(~65s)golangci-lint run ./internal/encryption/...(0 issues)CipherAPI does not bake storage/raft AAD assumptions in (§4.1 vs §4.2 layouts must remain orthogonal).ReservedKeyID = 0is rejected at all entry points (Cipher.Encrypt,Cipher.Decrypt,Keystore.Set).