An unproven device may not name a group
The announcement has always been refused from an unproven sender (audit C2). An ordinary group envelope carrying `gname` was not — so the weaker of the two routes decided what a group is called. On an unproven device `fromUser` is a CLAIM: the binding says that device belongs to somebody else. The roster check then asks "is the person they are pretending to be in this group", which answers yes for any member the attacker cares to name — and the group heading in someone's chat list is a good place to put a lie, because the reader has no reason to doubt it. `Received` now carries `senderProven` so a caller acting on the sender can ask, and `Session.receive` gates the adoption on it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
fc12aaa591
commit
2162262159
@@ -753,11 +753,20 @@ class Session(
|
|||||||
// the repair path for it. The announcement sent at creation
|
// the repair path for it. The announcement sent at creation
|
||||||
// reaches only who was online and only the devices that existed
|
// reaches only who was online and only the devices that existed
|
||||||
// then; a phone set up later would otherwise sit on the relay's
|
// then; a phone set up later would otherwise sit on the relay's
|
||||||
// generic placeholder for ever. The membership check above has
|
// generic placeholder for ever.
|
||||||
// already run, so this is not a way in for a stranger.
|
//
|
||||||
|
// 🔴 **Proven senders only, exactly like the announcement.**
|
||||||
|
// `consumeControl` refuses an unproven device, and for a while
|
||||||
|
// this path did not — so the weaker of the two routes decided
|
||||||
|
// what a group is called. On an unproven device `fromUser` is a
|
||||||
|
// claim, and the roster check above then asks "is the person
|
||||||
|
// they are pretending to be in this group", which answers yes
|
||||||
|
// for any member they care to name.
|
||||||
|
if (message.senderProven) {
|
||||||
message.content.raw?.optString("gname")?.takeIf { it.isNotEmpty() }
|
message.content.raw?.optString("gname")?.takeIf { it.isNotEmpty() }
|
||||||
?.let { adoptGroupName(gid, it) }
|
?.let { adoptGroupName(gid, it) }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// 🔴 **A federated message files under its ADDRESS.** Their relay
|
// 🔴 **A federated message files under its ADDRESS.** Their relay
|
||||||
// told ours nothing about who sent it, so `fromUser` is 0 — filing
|
// told ours nothing about who sent it, so `fromUser` is 0 — filing
|
||||||
// by it would drop every message from every other relay into one
|
// by it would drop every message from every other relay into one
|
||||||
|
|||||||
@@ -93,6 +93,17 @@ class Conversations(
|
|||||||
* a sealed message's sender on a path that skips that.
|
* a sealed message's sender on a path that skips that.
|
||||||
*/
|
*/
|
||||||
val sealed: Boolean = false,
|
val sealed: Boolean = false,
|
||||||
|
/**
|
||||||
|
* Whether the ratchet tied this sender to an identity we already hold.
|
||||||
|
*
|
||||||
|
* 🔴 **False means [fromUser] is a CLAIM.** The device's binding says
|
||||||
|
* it belongs to somebody else, so anything keyed on the sender —
|
||||||
|
* "is this person a member of that group", "is this their key" — is
|
||||||
|
* asking about the person they are pretending to be. Control traffic is
|
||||||
|
* refused outright at this layer; callers that act on the sender for
|
||||||
|
* any other reason have to check this themselves.
|
||||||
|
*/
|
||||||
|
val senderProven: Boolean = true,
|
||||||
/**
|
/**
|
||||||
* When the relay accepted this envelope, epoch millis, 0 = unknown.
|
* When the relay accepted this envelope, epoch millis, 0 = unknown.
|
||||||
* Carried for call signalling — see [RelayClient.Incoming.createdAt].
|
* Carried for call signalling — see [RelayClient.Incoming.createdAt].
|
||||||
@@ -1114,7 +1125,8 @@ class Conversations(
|
|||||||
}
|
}
|
||||||
if (fromAddress != null) vault.rememberRemoteAddress(fromAddress)
|
if (fromAddress != null) vault.rememberRemoteAddress(fromAddress)
|
||||||
out += Received(message.id, fromUser, fromDevice, content,
|
out += Received(message.id, fromUser, fromDevice, content,
|
||||||
sealed = sealed, at = message.createdAt,
|
sealed = sealed, senderProven = senderProven,
|
||||||
|
at = message.createdAt,
|
||||||
fromAddress = fromAddress)
|
fromAddress = fromAddress)
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
// Deliberately swallowed and skipped. One undecryptable message
|
// Deliberately swallowed and skipped. One undecryptable message
|
||||||
|
|||||||
@@ -292,6 +292,27 @@ class GroupNameTest {
|
|||||||
assertEquals("Book club, Tuesdays", current.groupName("g1"))
|
assertEquals("Book club, Tuesdays", current.groupName("g1"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 🔴 **`Received` carries whether the sender was proven, because a caller
|
||||||
|
* that acts on `fromUser` has to be able to ask.** An unproven device's
|
||||||
|
* sender id is a claim — the binding says it belongs to somebody else —
|
||||||
|
* so a roster check keyed on it asks "is the person they are pretending to
|
||||||
|
* be in this group", which answers yes for any member they name.
|
||||||
|
* `Session.receive` gates group-name adoption on this.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
fun `received messages say whether the sender was proven`() {
|
||||||
|
// The default is the safe-looking one, so the field is asserted at both
|
||||||
|
// values: a default that silently reads "proven" for everything would
|
||||||
|
// make every gate built on it a no-op.
|
||||||
|
val proven = Conversations.Received(1L, 42L, "dev", Content("hi"))
|
||||||
|
assertTrue(proven.senderProven)
|
||||||
|
|
||||||
|
val unproven = Conversations.Received(
|
||||||
|
1L, 42L, "dev", Content("hi"), senderProven = false)
|
||||||
|
assertFalse(unproven.senderProven)
|
||||||
|
}
|
||||||
|
|
||||||
// -- Where it is kept ------------------------------------------------------
|
// -- Where it is kept ------------------------------------------------------
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
Reference in New Issue
Block a user