package com.peachpasswords.vault import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test /** * PB-26-120: the duress design promises that a decoy session looks like an * ordinary installation and that everything it writes stays in the decoy. The * vault settings record — which carries the screen-capture control and the * favicon consent flag — is shared, plaintext state loaded at every session * start, and it broke both halves of that promise. */ class DecoySettingsIsolationTest { private class MemStorage : VaultStorage { var slotA: StoredVault? = null var slotB: String? = null private var ver: Long = 0 private var settings = VaultSettings() override fun loadSlotA() = slotA override fun hasVault() = slotA != null override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true } override fun loadSlotB() = slotB override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true } var decoy: StoredVault? = null override fun loadDecoySlot() = decoy override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true } override fun hasDecoySlot() = decoy != null override fun clearDecoySlot() { decoy = null } override fun version() = ver override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver } override fun loadSettings() = settings override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true } var history = VaultHistory() override fun loadVaultHistory() = history override fun writeVaultHistory(h: VaultHistory): Boolean { history = h; return true } override fun clearVaultHistory() { history = VaultHistory() } override fun clearVault(): Boolean { slotA = null; slotB = null; decoy = null; ver = 0 settings = VaultSettings(); history = VaultHistory(); return true } } private val mainPassword = "correct-horse-battery-staple" private val duressPassword = "open-the-decoy-vault-456" @Test fun a_decoy_session_neither_reads_nor_writes_the_real_settings() = runBlocking { val store = MemStorage() val repo = VaultRepository(store) repo.createVault(mainPassword) // The real vault deliberately differs from the defaults. repo.updateSettings(repo.settings.value.copy(allowScreenshots = true, sortOrder = "az")) assertTrue("fixture: the real record carries the change", store.loadSettings().allowScreenshots) repo.setDuressPassphrase(duressPassword) repo.lock() val duress = repo.tryUnlockDuress(duressPassword)!! assertEquals(UnlockResult.Success, repo.commitDuressUnlock(duress)) // Read side: the decoy shows its own defaults, not the real configuration. assertFalse("decoy must not surface the real capture setting", repo.settings.value.allowScreenshots) assertEquals("url-first", repo.settings.value.sortOrder) // Write side: a decoy edit stays in memory and never reaches the record // the real vault loads at its next unlock. repo.updateSettings(repo.settings.value.copy(breachMonitoring = false)) assertTrue("decoy write must not persist", store.loadSettings().breachMonitoring) assertTrue("the real record is untouched", store.loadSettings().allowScreenshots) assertFalse("the decoy's own view still reflects the edit", repo.settings.value.breachMonitoring) } }