1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertFalse6import org.junit.Assert.assertNotNull7import org.junit.Assert.assertNull8import org.junit.Assert.assertTrue9import org.junit.Test101112private class DuressFakeStorage : VaultStorage {13 private var slotA: StoredVault? = null14 private var slotB: String? = null15 private var ver: Long = 016 private var settings = VaultSettings()17 override fun loadSlotA() = slotA18 override fun hasVault() = slotA != null19 override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }20 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }21 private var decoy: StoredVault? = null22 override fun loadDecoySlot() = decoy23 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }24 override fun hasDecoySlot() = decoy != null25 override fun clearDecoySlot() { decoy = null }26 override fun version() = ver27 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }28 override fun loadSettings() = settings29 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }30 override fun clearVault() { slotA = null; slotB = null; ver = 0; settings = VaultSettings() }31}32333435363738class DuressSlotBTest {39 private val real = "the-real-password-123"40 private val duressPass = "open-the-decoy-456"4142 private fun configuredRepo(): VaultRepository = runBlocking {43 val repo = VaultRepository(DuressFakeStorage())44 repo.createVault(real)45 repo.addEntry(VaultEntry(id = "", title = "Secret", type = "login", password = "pw", createdAt = 0, modifiedAt = 0))46 assertTrue("duress passphrase sealed into slot B", repo.setDuressPassphrase(duressPass))47 repo.lock()48 repo49 }5051 @Test fun real_password_opens_slot_a_only() = runBlocking {52 val repo = configuredRepo()53 assertNotNull("real password opens slot A", repo.tryUnlockSlotA(real))54 assertNull("real password does NOT open the duress slot", repo.tryUnlockDuress(real))55 }5657 @Test fun duress_passphrase_opens_decoy_only() = runBlocking {58 val repo = configuredRepo()59 assertNull("duress passphrase does NOT open slot A", repo.tryUnlockSlotA(duressPass))60 val duress = repo.tryUnlockDuress(duressPass)61 assertNotNull("duress passphrase opens the duress slot", duress)62 assertEquals(UnlockResult.Success, repo.commitDuressUnlock(duress!!))63 assertTrue(repo.isUnlocked.value)64 assertEquals("decoy vault is empty — no real entries leak", 0, repo.getEntries().size)65 }6667 @Test fun wrong_password_opens_neither() = runBlocking {68 val repo = configuredRepo()69 assertNull(repo.tryUnlockSlotA("totally-wrong"))70 assertNull(repo.tryUnlockDuress("totally-wrong"))71 }7273 @Test fun duress_passphrase_cannot_equal_real_password() = runBlocking {74 val repo = VaultRepository(DuressFakeStorage())75 repo.createVault(real)76 assertFalse("a duress passphrase matching the real password is rejected", repo.setDuressPassphrase(real))77 }7879 @Test fun clear_duress_removes_the_slot() = runBlocking {80 val repo = configuredRepo()81 assertEquals(UnlockResult.Success, repo.unlock(real))82 assertTrue(repo.clearDuress())83 repo.lock()84 assertNull("duress slot gone after clear", repo.tryUnlockDuress(duressPass))85 }8687 @Test fun `HOLE 5 the duress vault has no sync identity and cannot sync`() = runBlocking {88 val repo = configuredRepo()8990 91 assertNotNull(repo.tryUnlockSlotA(real))92 assertEquals(UnlockResult.Success, repo.unlock(real))93 val realVaultId = repo.syncVaultId()94 assertNotNull("the real vault syncs", realVaultId)95 repo.lock()9697 98 99 100 val duress = repo.tryUnlockDuress(duressPass)101 assertNotNull(duress)102 assertEquals(UnlockResult.Success, repo.commitDuressUnlock(duress!!))103 assertTrue(repo.isUnlocked.value)104 assertNull("the duress vault must expose no sync key", repo.syncMasterKey())105 assertNull("the duress vault must expose no vault id", repo.syncVaultId())106107 108 109 val adapter = com.peachpasswords.sync.VaultSyncAdapter(repo, ourDeviceId = "decoy-device")110 var threw = false111 try {112 adapter.computeVaultId()113 } catch (_: Exception) {114 threw = true115 }116 assertTrue("the decoy vault must not produce a vault id for sync", threw)117 }118}