DecoySettingsIsolationTest.kt

3.6 KB · raw

1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertFalse6import org.junit.Assert.assertTrue7import org.junit.Test89/**10 * PB-26-120: the duress design promises that a decoy session looks like an11 * ordinary installation and that everything it writes stays in the decoy. The12 * vault settings record — which carries the screen-capture control and the13 * favicon consent flag — is shared, plaintext state loaded at every session14 * start, and it broke both halves of that promise.15 */16class DecoySettingsIsolationTest {1718    private class MemStorage : VaultStorage {19        var slotA: StoredVault? = null20        var slotB: String? = null21        private var ver: Long = 022        private var settings = VaultSettings()23        override fun loadSlotA() = slotA24        override fun hasVault() = slotA != null25        override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }26        override fun loadSlotB() = slotB27        override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }28        var decoy: StoredVault? = null29        override fun loadDecoySlot() = decoy30        override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }31        override fun hasDecoySlot() = decoy != null32        override fun clearDecoySlot() { decoy = null }33        override fun version() = ver34        override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }35        override fun loadSettings() = settings36        override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }37        var history = VaultHistory()38        override fun loadVaultHistory() = history39        override fun writeVaultHistory(h: VaultHistory): Boolean { history = h; return true }40        override fun clearVaultHistory() { history = VaultHistory() }41        override fun clearVault(): Boolean { slotA = null; slotB = null; decoy = null; ver = 042        settings = VaultSettings(); history = VaultHistory(); return true }43    }4445    private val mainPassword = "correct-horse-battery-staple"46    private val duressPassword = "open-the-decoy-vault-456"4748    @Test fun a_decoy_session_neither_reads_nor_writes_the_real_settings() = runBlocking {49        val store = MemStorage()50        val repo = VaultRepository(store)51        repo.createVault(mainPassword)5253        // The real vault deliberately differs from the defaults.54        repo.updateSettings(repo.settings.value.copy(allowScreenshots = true, sortOrder = "az"))55        assertTrue("fixture: the real record carries the change", store.loadSettings().allowScreenshots)5657        repo.setDuressPassphrase(duressPassword)58        repo.lock()59        val duress = repo.tryUnlockDuress(duressPassword)!!60        assertEquals(UnlockResult.Success, repo.commitDuressUnlock(duress))6162        // Read side: the decoy shows its own defaults, not the real configuration.63        assertFalse("decoy must not surface the real capture setting", repo.settings.value.allowScreenshots)64        assertEquals("url-first", repo.settings.value.sortOrder)6566        // Write side: a decoy edit stays in memory and never reaches the record67        // the real vault loads at its next unlock.68        repo.updateSettings(repo.settings.value.copy(breachMonitoring = false))69        assertTrue("decoy write must not persist", store.loadSettings().breachMonitoring)70        assertTrue("the real record is untouched", store.loadSettings().allowScreenshots)71        assertFalse("the decoy's own view still reflects the edit", repo.settings.value.breachMonitoring)72    }73}