Audit LOW: empty offer, attachment id, QR binding, skipped-key order

Same empty-offer and attachment-id rules as iOS. The QR scanner now
binds a scanned code by its fnid rather than the handle even on the home
relay — the fnid certifies itself, a handle is a name the relay maps,
so resolving by handle handed the relay the choice of who you scanned.
And a skipped message key is consumed only after its tag verifies: it
was defused today by the caller discarding the session, but any future
in-memory ratchet cache would have turned it into real message loss.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Paul Hitt
2026-08-29 09:13:25 -04:00
co-authored by Claude Opus 4.8
parent cbe2292d3c
commit 130edd7019
4 changed files with 44 additions and 9 deletions
@@ -201,10 +201,23 @@ class DoubleRatchet private constructor(
val aad = associatedData + message.header.canonicalJson()
// A key held for a message that never arrived, or arrived late.
//
// 🔴 **Removed only AFTER the tag verifies (audit LOW).** Consuming it
// first meant a forged ciphertext for a stashed (dh, n) burned the key
// the real delayed message needed — the genuine message could then
// never be opened. Today the caller discards the whole session object
// on a failed decrypt, so the loss never reaches the vault; that is a
// property of the CALLER, and the moment anything caches a ratchet in
// memory it would become a real message-loss bug. iOS has always
// removed after the open for exactly this reason.
val skippedKey = SkippedKey(Bytes.b64(message.header.dh), message.header.n)
skipped.remove(skippedKey)?.let { mk ->
skipped[skippedKey]?.let { mk ->
val (aesKey, nonce) = aeadParams(mk)
return Aead.open(aesKey, nonce + message.ciphertext, aad)
val plaintext = Aead.open(aesKey, nonce + message.ciphertext, aad)
// LinkedHashMap keeps insertion order, so removing from it is the
// whole eviction bookkeeping — there is no separate order list.
skipped.remove(skippedKey)
return plaintext
}
// A new ratchet key from the far side: catch up on their old chain,