feat(crypto+store): single AAD-bound enc:v1 format + ULID identifiers everywhere (spec 20)#513
Conversation
…l callers (spec 20, F-06)
…ed (#504) Spec 20 stage 1 / F-06 (WS10 deferral): collapse the two at-rest formats (nil-AAD enc:v1 + AAD-bound enc:v2) into ONE AAD-bound AES-256-GCM format under enc:v1. - crypto: Encrypt/Decrypt (nil-AAD) deleted; EncryptWithContext refuses an empty AAD; DecryptWithContext accepts only enc:v1 (+ plaintext passthrough) and errors LOUDLY on any other enc:* tag — a retired enc:v2 blob must never silently pass through as plaintext. New RowAAD(rowID, purpose) + shared Purpose* constants so write and read paths cannot drift. - IdP client secrets bind to RowAAD(provider_id, idp-client-secret) (create/update handlers, both SSO decrypt sites); TOTP secrets bind to RowAAD(user_id, totp-secret) (setup + both verify decrypts); testutil factories seed with the same AADs. - New archtest TestAEADStaysInCryptoPackage: AEAD Seal/Open (at AEAD arity) forbidden outside internal/crypto, literal-nil AAD forbidden at *WithContext call sites — the compile-time F-06 anti-regression. - lps_sealed_transport test updated to the enc:v1 prefix. Red-checked: prefix pin fails when flipped back to v2; retired-format and wrong-AAD paths covered by unit tests. Beta Path A: pre-change ciphertext requires reprovision (documented in the error itself).
…c regenerate + all caller fixes (spec 20, F-15)
…ID, luks tokens Go-minted; ack validates alert_id (spec 20, F-15) - lps_passwords/luks_keys projection rows take the rotating event's ULID as id (deterministic under replay — a rebuild reproduces the same row ids); luks_tokens mint a ULID at creation. - SecurityAlertAckParamsFromEvent validates alert_id as a ULID again — the fail-loud contract the retired ::uuid cast enforced and the malformed-alert-id test pins (fixture corrected to a 26-char ULID). - google/uuid is gone from all first-party code; only an indirect module dependency remains.
…ty fixture covers them; ADR 0030 (#504) The extended full-fidelity fixture (lps password, luks key, security alert + ack) caught the next replay-infidelity layer after the ULID ids: created_at DEFAULT now() re-minted on rebuild. The appliers now supply created_at from the event's occurred_at, so the rows reproduce byte-identically. ADR 0030 records the single-format + ULID decision (supersedes ADR 0009's dual-version framing).
|
Warning Review limit reached
Next review available in: 48 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis PR unifies at-rest encryption into a single AAD-bound ChangesAAD-bound Encryption Format
ULID-only Identifiers
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ 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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@internal/archtest/ulid_only_test.go`:
- Around line 56-84: The migration scan in ulid_only_test.go ignores the result
of f.Close(), which triggers errcheck. Update the file-handling logic around
os.Open, bufio.NewScanner, and sc.Err() so the close error is checked and
reported with t.Fatalf or otherwise handled consistently with the existing
open/scan error paths.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 12b2fc80-4d87-412b-96ec-4097f3cf904a
⛔ Files ignored due to path filters (6)
go.sumis excluded by!**/*.suminternal/store/generated/events.sql.gois excluded by!**/generated/**internal/store/generated/lps.sql.gois excluded by!**/generated/**internal/store/generated/luks.sql.gois excluded by!**/generated/**internal/store/generated/models.gois excluded by!**/generated/**internal/store/generated/security_alerts.sql.gois excluded by!**/generated/**
📒 Files selected for processing (43)
cmd/control/valkey.godocs/adr/0030-single-aad-at-rest-format-ulid-ids.mdgo.modinternal/api/audit_handler.gointernal/api/idp_handler.gointernal/api/lps_sealed_transport_test.gointernal/api/sso_handler.gointernal/api/totp_handler.gointernal/archtest/aead_containment_test.gointernal/archtest/ulid_only_test.gointernal/crypto/crypto.gointernal/crypto/crypto_test.gointernal/projectors/action_set_test.gointernal/projectors/action_test.gointernal/projectors/assignment_test.gointernal/projectors/compliance_policy_test.gointernal/projectors/compliance_test.gointernal/projectors/definition_test.gointernal/projectors/device_group_test.gointernal/projectors/device_test.gointernal/projectors/execution_test.gointernal/projectors/identity_provider_test.gointernal/projectors/lps_password_listener.gointernal/projectors/luks_key_listener.gointernal/projectors/role_test.gointernal/projectors/security_alert.gointernal/projectors/security_alert_test.gointernal/projectors/totp_test.gointernal/projectors/user_group_test.gointernal/projectors/user_test.gointernal/search/index.gointernal/store/luks.gointernal/store/migrations/002_event_store.sqlinternal/store/migrations/004_devices.sqlinternal/store/postgres/lps.gointernal/store/postgres/luks.gointernal/store/queries/events.sqlinternal/store/queries/lps.sqlinternal/store/queries/luks.sqlinternal/store/queries/security_alerts.sqlinternal/store/rebuild_fidelity_test.gointernal/store/store.gointernal/testutil/factories_idp.go
💤 Files with no reviewable changes (1)
- internal/projectors/totp_test.go
Spec 20 (#513) switched identifiers from uuid to text ULIDs by editing the already-shipped 002/004 migration files in place rather than adding a forward migration. goose tracks applied migrations by version number, not content, so a database created on v2026.07 (which recorded 002/004 as applied with uuid columns) never re-runs them — it keeps the uuid columns while the code now mints ULID strings, and the first AppendEvent after upgrade fails with 22P02 (invalid input syntax for type uuid). Migration 016 introspects and converts every remaining uuid-typed column in the public schema to text, working around the single security_alerts_projection.event_id -> events.id FK. Idempotent: a no-op on fresh v2026.08 databases where the columns are already text, verified via goose up (version 16) and a second-run.
Closes #504. Implements spec 20 — the last spec-19 prerequisite (after #511/#512). Spec 19's per-user DEK envelope wraps under this single format.
Stage 1 — one at-rest format (F-06, the WS10 deferral)
internal/cryptoships ONE format: AAD-bound AES-256-GCM underenc:v1. The naked nil-AADEncrypt/Decryptpair is deleted;EncryptWithContextrefuses an empty AAD at runtime;DecryptWithContextopens onlyenc:v1and errors loudly on any otherenc:*tag (reporting only the format tag, never ciphertext) — a retiredenc:v2blob must never silently pass through as plaintext. Plain non-prefixed values still pass through (pre-encryption plaintext + empty-string identity).RowAAD(rowID, purpose)+ sharedPurpose*constants: IdP client secrets bindidp_id|idp-client-secret(create/update + both SSO decrypts), TOTP secrets binduser_id|totp-secret(setup + both verify paths). LPS/LUKS keepSecretAAD. testutil factories seed with the same AADs as the read paths.TestAEADStaysInCryptoPackage: AEADSeal/Open(at AEAD arity) forbidden outsideinternal/crypto; literal-nil AAD forbidden at*WithContextcall sites.Stage 2 — ULID identifiers (F-15)
events.id,lps_passwords_projection.id,luks_keys_projection.id,luks_tokens.id,security_alerts_projection.event_id: uuid → text ULID.gen_random_uuid()defaults removed — the DB never mints a random identifier.AppendEvent/AppendEventWithVersionmint the event ULID in Go; secret-history projection rows take the rotating event's ULID as their id andcreated_atfrom the event'soccurred_at— so a rebuild reproduces them byte-identically. The extended full-fidelity fixture (lps password, luks key, security alert + ack) proves it; before this change those ids/timestamps were silently non-deterministic under replay (invisible only because the fixture left those tables empty).SecurityAlertAckParamsFromEventvalidatesalert_idas a ULID — preserving the fail-loud contract of the retired::uuidcast, pinned by the existing malformed-alert-id test.google/uuidis gone from all first-party code (only an indirect module dep remains). GuardTestULIDOnlyIdentifiers: nogoogle/uuidimport anywhere (generated code included, so a uuid column can't sneak back via sqlc), no uuid column type /gen_random_uuid()in migrations. Empty allowlist — red-checked at 13 findings before the migration.Migration stance
Beta Path A (per the spec): reprovision; no dual-read shim, no backfill. Pre-change ciphertext fails decryption with an error that names the retired tag; pre-change uuid rows are discarded with the reprovision.
Verification
go vet,staticcheck,gofmt,docref checkclean; fullgo test ./... -count=1: 33 packages ok, 0 failures.coderabbit reviewon the rebased branch: no findings.Summary by CodeRabbit
New Features
Bug Fixes
Tests