From 8842479bb8deb3d5a8bf003bde37a929df66fa39 Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Fri, 9 Oct 2026 13:42:42 -0400 Subject: [PATCH] Another account on the same install starts with an empty vault Found on the emulator against a local relay: after a plain sign-out of luke1009 and a sign-up as mira1009 on the same install, mira saw luke's conversation with lina1009, carried andy1009's handle certificate from two accounts earlier, and sent her first message to lina on luke's ratchet session. The iPhone could not open it ("no session for envelope 95 from a26ac1d7") and mira's first contact never arrived. Main has the same sign-in path: a different account got a new device identity (android#22) and kept every other record. AccountSwitch.forgetPreviousAccountIfSwitching empties the vault when a different account (another user id or relay) signs in, before its device is made. The same account signing back in keeps everything, as before. The last account's device identity was already being replaced at that point, so nothing forgotten was still usable by it. AnotherAccountStartsWithNothingTest: 2 red, then 4/4 green. Co-Authored-By: Claude Opus 5.5 --- .../flamenet/messenger/app/AccountSwitch.kt | 28 +++++++ .../messenger/app/MessengerViewModel.kt | 1 + .../AnotherAccountStartsWithNothingTest.kt | 78 +++++++++++++++++++ 3 files changed, 107 insertions(+) create mode 100644 app/src/main/kotlin/io/flamenet/messenger/app/AccountSwitch.kt create mode 100644 app/src/test/kotlin/io/flamenet/messenger/app/AnotherAccountStartsWithNothingTest.kt diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/AccountSwitch.kt b/app/src/main/kotlin/io/flamenet/messenger/app/AccountSwitch.kt new file mode 100644 index 0000000..913192f --- /dev/null +++ b/app/src/main/kotlin/io/flamenet/messenger/app/AccountSwitch.kt @@ -0,0 +1,28 @@ +package io.flamenet.messenger.app + +import io.flamenet.e2e.Vault + +/** What happens to the vault when somebody signs in. */ +object AccountSwitch { + /** + * Before a different account takes over this install, forget the last + * one's records: its conversations, sessions, delivery keys, drafts and + * handle certificate. A plain sign-out keeps the vault so the SAME person + * can sign back in; another account on the same install got a new device + * but inherited the rest, saw the last account's conversations and sent + * on its ratchet sessions, which the far end could not open. The last + * account's device was already replaced at this point, so what is + * forgotten could not have been used by it again. + * + * True when something was forgotten. + */ + fun forgetPreviousAccountIfSwitching( + vault: Vault, + previous: Vault.Credential?, + sameAccount: Boolean, + ): Boolean { + if (previous == null || sameAccount) return false + vault.forgetEverything() + return true + } +} 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 99d37f7..29d10ee 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt @@ -620,6 +620,7 @@ class MessengerViewModel( (previous.userId == account.userId && previous.relayBaseUrl == config.baseUrl) val storedIdentity = if (sameAccount) vault.loadIdentity() else null if (!sameAccount) identity = null + AccountSwitch.forgetPreviousAccountIfSwitching(vault, previous, sameAccount) val device = identity diff --git a/app/src/test/kotlin/io/flamenet/messenger/app/AnotherAccountStartsWithNothingTest.kt b/app/src/test/kotlin/io/flamenet/messenger/app/AnotherAccountStartsWithNothingTest.kt new file mode 100644 index 0000000..657f410 --- /dev/null +++ b/app/src/test/kotlin/io/flamenet/messenger/app/AnotherAccountStartsWithNothingTest.kt @@ -0,0 +1,78 @@ +package io.flamenet.messenger.app + +import io.flamenet.e2e.Vault +import java.io.File +import javax.crypto.KeyGenerator +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue + +/** + * Found on the emulator against a local relay, 2026-10-09: a plain sign-out + * keeps the vault so the same person can sign back in, and a DIFFERENT + * account signing in on the same install got a new device identity but kept + * everything else. mira1009 saw luke1009's conversation with lina1009 and + * sent her first message on luke's ratchet session, which the iPhone could not open ("no session for + * envelope 95"). A different account now starts with an empty vault. + */ +class AnotherAccountStartsWithNothingTest { + private class MapStorage : Vault.Storage { + val records = mutableMapOf() + + override fun read(name: String) = records[name] + + override fun write( + name: String, + bytes: ByteArray, + ) { + records[name] = bytes + } + + override fun delete(name: String) { + records.remove(name) + } + + override fun names() = records.keys.toList() + } + + private fun vault() = Vault(MapStorage(), KeyGenerator.getInstance("AES").apply { init(256) }.generateKey()) + + private fun credential(user: Long) = Vault.Credential("http://127.0.0.1:18810", "u$user", "pw", user) + + @Test + fun `a different account finds none of the last one's records`() { + val v = vault() + v.saveCredential(credential(3)) + v.saveGroupName("g1", "luke's group") + v.saveDraft("user:4", "for lina") + assertTrue(AccountSwitch.forgetPreviousAccountIfSwitching(v, credential(3), sameAccount = false)) + assertNull(v.groupName("g1")) + assertEquals(null, v.draft("user:4")) + assertNull(runCatching { v.loadCredential() }.getOrNull()) + } + + @Test + fun `the same account keeps everything`() { + val v = vault() + v.saveCredential(credential(3)) + v.saveGroupName("g1", "luke's group") + assertFalse(AccountSwitch.forgetPreviousAccountIfSwitching(v, credential(3), sameAccount = true)) + assertNotNull(v.groupName("g1")) + } + + @Test + fun `a first sign-in has nothing to forget`() { + assertFalse(AccountSwitch.forgetPreviousAccountIfSwitching(vault(), null, sameAccount = true)) + } + + @Test + fun `sign-in forgets before the new identity is saved`() { + val vm = File("src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt").readText() + val forget = vm.indexOf("AccountSwitch.forgetPreviousAccountIfSwitching(") + val newIdentity = vm.indexOf("DeviceIdentity.create().also {") + assertTrue(forget in 0 until newIdentity, "sign-in does not forget the last account before making this one's device") + } +}