Refuse a home record older than one already seen (§20.4, Android half)
Both remaining ⚪ protocol rows turned out to be stale. `AccountKdf.localAccountSalt` is pinned to the published vector and wired into sign-in for a local relay; `MailboxHoming` and `AccountIdentity.publishHomes` have shipped the publish and read halves for a while. PARITY.md was describing a gap that had closed, which is its own kind of wrong — a definition of done that overstates the work left is one people stop reading. 🔴 What was genuinely missing was on the READING side, and on BOTH clients. An older home record is GENUINELY SIGNED — the account holder published it, before they added a second relay. So a relay that wants to be somebody's only mailbox does not have to forge anything: it serves the record it saw first, that record verifies perfectly, and the sender delivers to one relay believing it is delivering to all of them. That is the attack multi-homing exists to survive, and the serial is what was added to stop it. The relay refuses a lower serial on WRITE. Nothing refused one on READ: `verifiedRelays` here and `verifyHomes` on iOS both checked the signature and stopped. Fixed on both — iOS in ios!116, this is the Kotlin half. The highest serial seen per address is remembered in the vault and a lower one is refused. Equal is fine; only strictly lower is a rollback. 🔴 The floor rises only for a record that VERIFIED, via a separate `verifiedSerial`. Otherwise a hostile relay could hand over an unsigned record carrying Int.MAX_VALUE, have the client raise its floor to it, and lock out every later genuine record — turning the rollback defence into a denial of service against the account holder. Tested. ⚠️ Sealed in the vault and keyed by a hash of the address: which addresses this device has looked up is exactly the metadata the vault exists to keep off a stolen phone. ⚠️ The refusal does not claim an attack. A relay that has genuinely fallen behind serves an old record for honest reasons, and nothing in a client can tell that from the other case. 8 unit tests. App builds, every JVM suite green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
455cdcd818
commit
6e4eca84e3
@@ -794,7 +794,23 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) {
|
||||
// survive, and showing it unchecked would make this screen a place
|
||||
// that quietly confirms the attack.
|
||||
runCatching {
|
||||
current.relay.homes(fnid)?.let(io.flamenet.client.AccountIdentity::verifiedRelays)
|
||||
val record = current.relay.homes(fnid) ?: return@runCatching null
|
||||
// 🔴 **And not older than one already seen.** An old record is
|
||||
// genuinely signed — published before a second home was added —
|
||||
// so a relay wanting to be somebody's only mailbox does not
|
||||
// have to forge anything; it just serves the record it saw
|
||||
// first. The signature check alone accepts that, which is what
|
||||
// the serial exists to stop.
|
||||
val floor = current.vault.homeSerial(fnid)
|
||||
val relays = io.flamenet.client.AccountIdentity
|
||||
.verifiedRelays(record, floor = floor)
|
||||
// ⚠️ Advanced only after the record was ACCEPTED. Recording the
|
||||
// serial of a record that failed verification would let a
|
||||
// hostile relay raise the floor with a forgery and lock out
|
||||
// every later genuine record.
|
||||
io.flamenet.client.AccountIdentity.verifiedSerial(record)
|
||||
?.let { if (relays != null) current.vault.saveHomeSerial(fnid, it) }
|
||||
relays
|
||||
}.getOrNull()
|
||||
}
|
||||
val readable = withContext(Dispatchers.IO) {
|
||||
|
||||
Reference in New Issue
Block a user