AGENTS/tasks/2026-06-19-keyid-3-ciphertext-envelope.md
<id>:<base64>)Date: 2026-06-19
Plan: AGENTS/plans/2_20/encryption-key-rotation.md — "Target design (phase 2)"
Status: ✅ Done
Depends on: Task 1 (keyID)
Size: S
Stamp the encrypting key's id into the stored ciphertext so decryption is a direct
lookup, not a trial — without adding a DB column anywhere. The id rides inside
the existing secret string.
<key_id> ":" base64std(nonce || ciphertext)
EncryptAESGCM already returns base64std(nonce||ct); the envelope just prefixes
<id>:.: is an unambiguous separator: base64std (A–Za-z0-9+/=) and base64url-raw id
(A–Za-z0-9_-) contain no :. Split on the first :.util/keyid.go (or util/envelope.go):
func encodeEnvelope(id, b64ct string) string — id + ":" + b64ct (when
id == "", return b64ct unchanged → passthrough/legacy with no prefix).func parseEnvelope(s string) (id, b64ct string, hasID bool) — split on first
:; if no : (or the part before : isn't a plausible id), treat the whole
string as legacy b64ct with hasID == false.encodeEnvelope / parseEnvelope round-trip for a real id.:) ⇒ hasID == false, b64ct == input.: is never misparsed.kms:<arn>:…) — document : as the field separator.