π€ Second opinion from a Fable agent on the one thing I said was worth taking from EscoDB β moving to a key-committing AEAD, and possibly XAES-256-GCM.
Its recommendation: don't. It corrected me on three points and I think it is right on all three.
1. The KCV is already sound key commitment, for our threat model
Every key-commitment attack β partitioning oracles, invisible salamanders β needs the attacker to choose or enumerate the keys the victim will open under. Walking our own compromise matrix:
W_rc,W_prf: KEKs from 128- and 256-bit full-entropy inputs. No candidate set exists, so no attack, commitment or not.W_pw: the only branch with a dictionary, and it is gated on the Account Key the server never sees. Even granting it, the client checksHMAC(KEK)[..16]before decrypting, and a 16-byte HMAC output matches exactly one candidate KEK β so a crafted k-key GCM ciphertext gets past it for at most one key. One oracle query tests one guess, which is what a committing AEAD gives.- Account keyset, vault keysets, items, and the future ECDH member wraps carry no KCV and need none: nobody but the client can hold or enumerate RK,
K_vaultor an ECDH-derived key, so a multi-key ciphertext cannot be built against them. - Salamanders in a shared vault: every member opens an item under the same
K_vault, so there is no "one ciphertext, two keys" pair. The only holder of two keys is the owner mid-rotation β who is the instance operator under decision 1 and can already serve different bytes to different members. Committing AEAD does not fix that; nothing short of a transparency log does.
The honest difference it names: a committing AEAD puts the commitment in the bytes, ours is a property of the decryption procedure (check KCV, then open). A second implementation that skips the check loses it β so that is discipline to enforce, not a risk we are carrying.
It also says DESIGN.md's emphasis is backwards. The KCV's load-bearing job is key confirmation β wrong key versus corrupted blob, and the PRF-mismatch detector. Commitment is a side effect that happens to be complete exactly where it is needed.
2. The "minimum change" I asked about is what we already do
I asked whether there was something cheaper than XAES β such as deriving both the AEAD key and a commitment from one secret via HKDF. That is the code: HMAC-SHA-256(KEK, label)[..16] is HKDF-Expand(KEK, label) minus a counter byte, i.e. the Albertini et al. key-derivation commitment.
If commitment is ever wanted for items and keysets, the right tool is not XAES and not encrypt-then-MAC: prefix 32 zero bytes to the plaintext and check them on open. Pure plaintext bytes, so Go, WebCrypto and CryptoKit GCM are all untouched; about five lines in pad.go and sealed.js; ~2^-256 commitment. Worth remembering rather than doing.
3. Nonces and timing
96-bit random nonces are a non-issue here by many orders of magnitude β ~6Γ10^-22 collision probability at 10^4 writes under K_vault, and rotation resets the count. The only realistic reuse path is RNG failure (a restored VM snapshot), and XAES does not fix that β it widens the nonce. AES-GCM-SIV would, and is in neither WebCrypto nor CryptoKit.
On timing, my premise was half wrong. I said the wire format was unset; the crypto contract is pinned β vectors/README.md says changing a value in that file is changing the protocol, and #19 pins the formats. What is unset is the JSON envelope. And AGENTS.md's test for reopening a settled decision is new facts: Albertini 2020, LenβGrubbsβRistenpart 2021 and XAES 2024 all predate DESIGN.md, which already names the non-committing property explicitly. There is no new fact here, only a re-read β which is a discussion, not a proposal.
One nit worth recording and not acting on
The KCV is unsalted, so two wrap rows with equal KEKs have equal KCVs. The only way that happens is two W_prf rows from providers returning the same PRF, telling the server "these two providers agree" β which the random nonces would otherwise hide. Salting with the wrap id removes it and changes the vectors for nothing.
Proposed close: a docs edit, not a redesign
Reword the KCV line in DESIGN.md so key confirmation is the stated primary job and commitment is what it also delivers on wrap rows; say that keysets and items have no commitment and need none, with the reason; and record "committing AEAD and XAES considered and rejected" with this reasoning so it is not relitigated. Optionally note in vectors/README.md that the KCV check is part of Open on every platform rather than a UX nicety.
Holding that edit until the second Fable review (storage posture, #23) reports β it touches the same section, and one dated edit beats two.