1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertFalse6import org.junit.Assert.assertNull7import org.junit.Assert.assertTrue8import org.junit.Test9101112131415161718class RecoveryEscrowIntegrationTest {1920 private class FakeStorage : VaultStorage {21 var slotA: StoredVault? = null22 var slotB: String? = null23 private var ver: Long = 024 private var settings = VaultSettings()25 override fun loadSlotA() = slotA26 override fun hasVault() = slotA != null27 override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }28 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }29 var decoy: StoredVault? = null30 override fun loadDecoySlot() = decoy31 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }32 override fun hasDecoySlot() = decoy != null33 override fun clearDecoySlot() { decoy = null }34 override fun version() = ver35 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }36 override fun loadSettings() = settings37 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }38 override fun clearVault(): Boolean { slotA = null; slotB = null; ver = 0; settings = VaultSettings(); return true }39 }4041 42 private fun recoveryWordsOf(repo: VaultRepository): List<String> {43 val key = repo.syncMasterKey()!!44 return try { rootKeyToWords(key) } finally { VaultCrypto.zero(key) }45 }4647 private fun login(title: String, password: String = "pw") =48 VaultEntry(id = "", title = title, type = "login", password = password, createdAt = 0, modifiedAt = 0)4950 51 @Test fun vault_creation_seals_recovery_escrow() = runBlocking {52 val storage = FakeStorage()53 val repo = VaultRepository(storage)54 repo.createVault("correct horse battery")55 56 assertFalse(repo.hasRecoveryEscrow())57 58 assertTrue(repo.setRecoveryWords(recoveryWordsOf(repo)))59 assertTrue(repo.hasRecoveryEscrow())60 }6162 63 @Test fun recovery_words_unlock_after_creation() = runBlocking {64 val storage = FakeStorage()65 val repo = VaultRepository(storage)66 repo.createVault("correct horse battery")67 repo.addEntry(login("GitHub"))68 val words = recoveryWordsOf(repo)69 repo.setRecoveryWords(words)70 repo.lock()7172 assertTrue(repo.unlockWithRecoveryWords(words))73 assertTrue(repo.getEntries().any { it.title == "GitHub" })74 }7576 77 @Test fun recovery_not_available_before_confirmation() = runBlocking {78 val storage = FakeStorage()79 val repo = VaultRepository(storage)80 repo.createVault("correct horse battery")81 val words = recoveryWordsOf(repo) 82 repo.lock()83 assertFalse(repo.hasRecoveryEscrow())84 assertFalse(repo.unlockWithRecoveryWords(words))85 }8687 88 @Test fun recovery_survives_password_change() = runBlocking {89 val storage = FakeStorage()90 val repo = VaultRepository(storage)91 repo.createVault("first-master-pass")92 repo.addEntry(login("Bank"))93 val words = recoveryWordsOf(repo)94 repo.setRecoveryWords(words)9596 assertTrue(repo.changePassword("first-master-pass", "second-master-pass"))97 repo.lock()9899 100 assertTrue(repo.unlockWithRecoveryWords(words))101 assertTrue(repo.resetPasswordWithRecoveryWords(words, "third-master-pass"))102 repo.lock()103 assertEquals(UnlockResult.Success, repo.unlock("third-master-pass"))104 assertTrue(repo.getEntries().any { it.title == "Bank" })105 }106}