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
@@ -724,12 +724,13 @@ internal fun ColumnScope.SignedIn(
onFound = { raw ->
scanning = false
parseScannedAddress(raw)?.let { card ->
// A same-relay code that carries the handle can go through
// the ordinary username lookup; anything else fills the
// field with the address it named.
scannedPrefill = if (card.handle != null &&
card.relay.equals(phase.relay, ignoreCase = true)
) card.handle else "${card.fnid}@${card.relay}"
// 🔴 **Bind by the fnid, never the handle (audit LOW).**
// The fnid is derived from the account key and certifies
// itself; a handle is a name the RELAY maps to an account,
// so resolving a scanned code by handle handed the relay
// the choice of who you just scanned. iOS always binds the
// address; this now matches it, same relay or not.
scannedPrefill = "${'$'}{card.fnid}@${'$'}{card.relay}"
addingContact = true
}
},