1package com.peachpasswords.security23import com.peachpasswords.vault.StoredVault4import com.peachpasswords.vault.UnlockResult5import com.peachpasswords.vault.VaultEntry6import com.peachpasswords.vault.VaultRepository7import com.peachpasswords.vault.VaultSettings8import com.peachpasswords.vault.VaultStorage9import kotlinx.coroutines.runBlocking10import org.junit.Assert.assertEquals11import org.junit.Assert.assertFalse12import org.junit.Assert.assertNull13import org.junit.Assert.assertTrue14import org.junit.Test1516171819202122class SecurityPropertiesTest {2324 private class Storage : VaultStorage {25 var slotA: StoredVault? = null26 var slotB: String? = null27 private var ver: Long = 028 private var settings = VaultSettings()29 override fun loadSlotA() = slotA30 override fun hasVault() = slotA != null31 override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }32 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }33 var decoy: StoredVault? = null34 override fun loadDecoySlot() = decoy35 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }36 override fun hasDecoySlot() = decoy != null37 override fun clearDecoySlot() { decoy = null }38 override fun version() = ver39 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }40 override fun loadSettings() = settings41 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }42 override fun clearVault(): Boolean { slotA = null; slotB = null; ver = 0; settings = VaultSettings(); return true }43 }4445 private fun repo() = VaultRepository(Storage()).also { runBlocking { it.createVault("real-master-pass") } }46 private fun login(t: String) = VaultEntry(id = "", title = t, type = "login", password = "p", createdAt = 0, modifiedAt = 0)4748 4950 @Test fun vault_key_is_null_after_lock() {51 val r = repo()52 assertTrue(r.currentVaultKey() != null)53 r.lock()54 assertNull(r.currentVaultKey())55 assertNull(r.syncMasterKey())56 assertFalse(r.isUnlocked.value)57 }5859 @Test fun entries_are_cleared_from_memory_after_lock() = runBlocking {60 val r = repo()61 r.addEntry(login("Secret"))62 r.lock()63 assertTrue(r.getEntries().isEmpty())64 }6566 6768 @Test fun duress_passphrase_opens_decoy_not_real_vault() = runBlocking {69 val r = repo()70 val real = r.addEntry(login("RealAccount"))71 r.setDuressPassphrase("duress-pass-xyz")72 r.lock()7374 val duress = r.tryUnlockDuress("duress-pass-xyz")!!75 assertEquals(UnlockResult.Success, r.commitDuressUnlock(duress))76 77 assertFalse(r.getEntries().any { it.id == real.id })78 assertTrue(r.getEntries().isEmpty())79 }8081 @Test fun real_password_does_not_match_duress_slot() = runBlocking {82 val r = repo()83 r.setDuressPassphrase("duress-pass-xyz")84 r.lock()85 86 assertNull(r.tryUnlockDuress("real-master-pass"))87 }8889 @Test fun duress_passphrase_does_not_open_real_slot() = runBlocking {90 val r = repo()91 r.setDuressPassphrase("duress-pass-xyz")92 r.lock()93 assertNull(r.tryUnlockSlotA("duress-pass-xyz"))94 }9596 9798 @Test fun destroy_wipes_vault_and_blocks_old_password() = runBlocking {99 val storage = Storage()100 val r = VaultRepository(storage)101 r.createVault("real-master-pass")102 r.addEntry(login("Gone"))103 r.destroyVault()104 assertFalse(r.hasVault())105 assertNull(storage.slotA)106 assertNull(storage.decoy)107 assertEquals(UnlockResult.Error("no_vault"), r.unlock("real-master-pass"))108 }109110 @Test fun destroy_also_clears_the_decoy_slot() = runBlocking {111 val storage = Storage()112 val r = VaultRepository(storage)113 r.createVault("real-master-pass")114 r.setDuressPassphrase("duress-pass-xyz")115 assertTrue(storage.decoy != null)116 r.destroyVault()117 assertNull(storage.decoy)118 }119120 121122 @Test fun recovery_not_available_without_escrow() = runBlocking {123 val r = repo()124 r.lock()125 assertFalse(r.hasRecoveryEscrow())126 }127128 129130 private class FailingClearStorage : VaultStorage {131 var slotA: StoredVault? = null132 var slotB: String? = null133 var decoy: StoredVault? = null134 private var ver: Long = 0135 private var settings = VaultSettings()136 override fun loadSlotA() = slotA137 override fun hasVault() = slotA != null138 override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }139 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }140 override fun loadDecoySlot() = decoy141 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }142 override fun hasDecoySlot() = decoy != null143 override fun clearDecoySlot() { decoy = null }144 override fun version() = ver145 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }146 override fun loadSettings() = settings147 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }148 override fun clearVault(): Boolean { return false }149 }150151 @Test fun destroy_vault_returns_false_when_clear_vault_fails() = runBlocking {152 val storage = FailingClearStorage()153 val r = VaultRepository(storage)154 r.createVault("real-master-pass")155 assertFalse("destroy must surface the wipe failure", r.destroyVault())156 }157}