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") + } +}