Count the sealed-to-unsealed fallback, and offer to refuse it (ios#16)
🔴 **Off by default, and that is the honest default rather than the lazy one.** An unsealed send is still end-to-end encrypted; what leaks is that this account sent something to that one. Refusing turns a metadata leak into an undelivered message, which for most people is the worse outcome — so the default degrades and says so, and the refusal is there for the people who would rather a message did not go than go attributable. The counters answer a question nobody could ask before. `Sent.sealed` says whether one send was sealed; the record says how often this happens to you, which is what somebody actually needs to decide whether the switch is worth throwing. ⚠️ Refusing costs nothing: `sealed` is false exactly when neither a capability nor a key was found, so there is no token to hand back. ⚠️ Both the policy and the counters are wired in ONE helper called from both sign-in paths. Two call sites building a `Session` and only one applying the policy is how a setting silently stops working on one of them. Taken by hand rather than cherry-picked — the original also rewrote a ChatScreens that has since changed underneath it. The settings switch for it is UI work still to come.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
package io.flamenet.e2e
|
||||
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.test.assertFalse
|
||||
import kotlin.test.assertNull
|
||||
import kotlin.test.assertTrue
|
||||
|
||||
/** Counting the sealed-to-unsealed fallback (§14.5, ios#16). */
|
||||
class SealingRecordTest {
|
||||
|
||||
@Test
|
||||
fun `a sealed send is counted`() {
|
||||
val r = SealingRecord().recording(wasSealed = true, at = 5)
|
||||
assertEquals(1, r.sealed)
|
||||
assertEquals(0, r.unsealed)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unsealed send is counted and stamped`() {
|
||||
val r = SealingRecord().recording(wasSealed = false, at = 1_700_000_000)
|
||||
assertEquals(1, r.unsealed)
|
||||
assertEquals(1_700_000_000, r.lastUnsealedAt)
|
||||
}
|
||||
|
||||
/** ⚠️ One good send afterwards does not unhappen the last bad one. */
|
||||
@Test
|
||||
fun `a sealed send does not clear the last unsealed stamp`() {
|
||||
val r = SealingRecord()
|
||||
.recording(wasSealed = false, at = 100)
|
||||
.recording(wasSealed = true, at = 200)
|
||||
assertEquals(100, r.lastUnsealedAt)
|
||||
}
|
||||
|
||||
/** 🔴 Zero sends is no data, not zero leakage. */
|
||||
@Test
|
||||
fun `no sends has no share rather than zero`() {
|
||||
assertNull(SealingRecord().unsealedShare)
|
||||
assertTrue(SealingRecord().summary.contains("Nothing sent"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the share is what went unsealed`() {
|
||||
var r = SealingRecord()
|
||||
repeat(3) { r = r.recording(wasSealed = true, at = 1) }
|
||||
r = r.recording(wasSealed = false, at = 1)
|
||||
assertEquals(0.25, r.unsealedShare!!, 0.0001)
|
||||
}
|
||||
|
||||
/**
|
||||
* 🔴 **Must not say the message was readable.** An unsealed send is still
|
||||
* end-to-end encrypted; the relay learns the sender, not the content.
|
||||
*/
|
||||
@Test
|
||||
fun `the summary distinguishes sender from content`() {
|
||||
val summary = SealingRecord().recording(wasSealed = false, at = 1).summary
|
||||
assertTrue(summary.contains("end-to-end encrypted"), summary)
|
||||
assertTrue(summary.contains("who sent them"), summary)
|
||||
assertTrue(summary.contains("not what they said"), summary)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a clean record says so without overclaiming`() {
|
||||
var r = SealingRecord()
|
||||
repeat(5) { r = r.recording(wasSealed = true, at = 1) }
|
||||
assertTrue(r.summary.contains("were sealed"), r.summary)
|
||||
assertFalse(r.summary.contains("%"), r.summary)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user