diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt index 36019f7..916452a 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/ChatScreens.kt @@ -855,8 +855,11 @@ fun ColumnScope.AccountScreen( SettingsRow( label = "Sealed sender", description = "Messages you send are addressed to the recipient without naming you. " - + "Replacing your delivery key is the only way to stop someone abusing it โ€” and " - + "it cuts off every contact until you next message them.", + + "Because nothing names the sender, the server cannot block one โ€” so blocking " + + "somebody now also cancels the one-use passes you gave them, which is the part " + + "that can be aimed at a single person. This replaces the shared key instead, " + + "which everybody holds: it is the way to cut off somebody you cannot identify, " + + "and it costs every contact until you next message them.", ) { KitChip(text = "New key", selected = false, onClick = model::rotateDeliveryKey, tag = "rotate-delivery-key") diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt index fae0eff..c736f5e 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt @@ -1015,6 +1015,24 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) { val done = withContext(Dispatchers.IO) { runCatching { current.api.setBlocked(userId, blocked) + // ๐Ÿ”ด **And take their sealed capabilities with it.** The relay + // cannot enforce a block against a sealed sender โ€” the request + // carries no sender to compare against a block list, and the + // send route says so itself โ€” so without this, blocking stops + // the identified path and nothing else, and a blocked person's + // sealed messages are dropped by this app after decryption + // rather than refused at the door. + // + // โš ๏ธ One of two doors, and this shuts one. They keep the shared + // delivery key of ยง14.4, which the relay also cannot attribute, + // until it is rotated โ€” and rotating costs every other contact + // a redistribution. Best-effort and after the block has landed: + // the block is the safety action, and failing it because a + // follow-up failed would leave somebody unblocked who believes + // otherwise. + if (blocked) { + runCatching { current.conversations.revokeDeliveryTokens(userId) } + } current.api.blocked().toSet() to current.roster() } } diff --git a/client/src/test/kotlin/io/flamenet/client/DeliveryTokenTest.kt b/client/src/test/kotlin/io/flamenet/client/DeliveryTokenTest.kt index a2980df..e35ae8b 100644 --- a/client/src/test/kotlin/io/flamenet/client/DeliveryTokenTest.kt +++ b/client/src/test/kotlin/io/flamenet/client/DeliveryTokenTest.kt @@ -271,6 +271,37 @@ class DeliveryTokenTest { assertEquals(first, vault.issuedTokens(42).toSet()) } + /** + * ๐Ÿ”ด **This is the act the whole section exists to make possible.** The + * relay cannot enforce a block against a sealed sender โ€” the request + * carries no sender to compare against a block list โ€” so before ยง21.4, + * blocking stopped the identified path and nothing else, and a blocked + * person's sealed messages were dropped by the app after decryption rather + * than refused at the door. + * + * The view model calls this alongside the block; asserted at this layer + * because it is where the behaviour lives. + */ + @Test + fun `revoking is what stops a sealed sender the relay cannot identify`() { + val c = withDeliveryKey() + c.send(toUserId = 42, text = "hello") + assertEquals(64, held.size) + + c.revokeDeliveryTokens(forUserId = 42) + + // Every capability that person held is gone from the relay, so their + // next sealed send is refused with no sender ever named. + assertEquals(0, held.size) + assertTrue(vault.issuedTokens(42).isEmpty()) + } + + /** Revoking a contact who never held any is not an error. */ + @Test + fun `revoking somebody with no batch does nothing and says so`() { + assertEquals(0, withDeliveryKey().revokeDeliveryTokens(forUserId = 99)) + } + // MARK: - Receiving a batch @Test