AGENTS/tasks/2026-06-19-keyid-6-backward-compat-migration.md
Date: 2026-06-19
Plan: AGENTS/plans/2_20/encryption-key-rotation.md — "Target design (phase 2)"
Status: ✅ Done
Depends on: Task 4 (defines the legacy branch); cross-cuts 2/5
Size: M — highest correctness risk (ciphertext format change)
Guarantee that switching to the key-id envelope never makes existing data unreadable. Define and test the exact compatibility contract.
<id>:) — written by pre-feature installs and by
phase 1. Must decrypt via the legacy path:
access_key_encryption (+ any still-configured phase-1
secondaries, trial — kept only for the migration window);vault rekey, the row is re-stamped with an id.access_key_encryption, option_encryption) keep
working: treated as a key in the keyset (compute its id) so new writes can stamp
it, and as the no-prefix decrypt key for legacy rows.access_key:{primary,secondary} / option_key:{…}) —
decide and document one of:
keys:/active: vs access_key:), mapping
primary→active and secondary→registry entries; orkeys:/active: (acceptable because phase 1 is
unreleased) and fail fast with a clear message on the old shape.
Recommendation: (b) unless phase 1 shipped to anyone — simpler, no dual
parser to maintain.util/config.go / util/keyring.go: implement the chosen file-shape policy and
the legacy no-prefix decrypt branch (the flat field becomes a keyset entry).util/keyset_compat_test.go)| Data written by | Config now | Expect |
|---|---|---|
| pre-feature (flat key, no prefix) | flat field only | decrypts (legacy) |
| pre-feature (flat key, no prefix) | keyset + flat as a key | decrypts (legacy), re-stamped on rekey |
| phase-1 (no prefix, primary key) | keyset | decrypts via legacy/flat |
| phase-2 (id prefix) | keyset | decrypts via id lookup |
id prefix, id absent from keys: | keyset | loud "key not found" |
vault rekey over mixed rows | keyset | all converge to active id; legacy count → 0 |
vault rekey finishes". During it, both decrypt
paths must be live. vault check (Task 5) shows the legacy count dropping to 0.vault rekey → vault check (0 legacy,
0 MISSING) → optionally drop the flat field / old keys.