1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertFalse6import org.junit.Assert.assertNotEquals7import org.junit.Assert.assertTrue8import org.junit.Test910111213141516171819202122class PasswordRotationAtomicityTest {2324 private class FaultStorage : VaultStorage {25 var slotA: StoredVault? = null26 var slotB: String? = null27 var failWriteSlotA = false28 var failWriteSlotB = false29 var failStamp = false30 private var ver: Long = 031 private var settings = VaultSettings()32 override fun loadSlotA() = slotA33 override fun hasVault() = slotA != null34 override fun writeSlotA(slot: StoredVault): Boolean {35 if (failWriteSlotA) return false36 slotA = slot37 return true38 }39 override fun loadSlotB() = slotB40 override fun writeSlotB(blobBase64: String): Boolean {41 if (failWriteSlotB) return false42 slotB = blobBase6443 return true44 }45 var decoy: StoredVault? = null46 override fun loadDecoySlot() = decoy47 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }48 override fun hasDecoySlot() = decoy != null49 override fun clearDecoySlot() { decoy = null }50 override fun version() = ver51 override fun stampLocalMutation(): Long {52 if (failStamp) throw RuntimeException("stamp failed")53 ver = maxOf(System.currentTimeMillis(), ver + 1)54 return ver55 }56 override fun loadSettings() = settings57 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }58 override fun loadVaultHistory() = VaultHistory()59 override fun writeVaultHistory(history: VaultHistory): Boolean = true60 override fun clearVault(): Boolean { slotA = null; slotB = null; ver = 0; settings = VaultSettings(); return true }61 }6263 private fun repoWithPassword(storage: FaultStorage, password: String): VaultRepository = runBlocking {64 val repo = VaultRepository(storage)65 repo.createVault(password)66 67 68 repo.addEntry(VaultEntry(id = "", title = "Keep me", type = "login", password = "p",69 createdAt = 0, modifiedAt = 0))70 71 72 val words = rootKeyToWords(repo.syncMasterKey()!!)73 assertTrue(repo.setRecoveryWords(words))74 repo75 }7677 private fun recoveryWordsOf(repo: VaultRepository): List<String> {78 val key = repo.syncMasterKey()!!79 return try { rootKeyToWords(key) } finally { VaultCrypto.zero(key) }80 }8182 8384 @Test fun change_password_with_slot_b_failure_keeps_old_key() = runBlocking {85 val storage = FaultStorage()86 val repo = repoWithPassword(storage, "old-pass")87 val slotABefore = storage.slotA88 val slotBBefore = storage.slotB89 storage.failWriteSlotB = true9091 92 93 94 assertFalse(repo.changePassword("old-pass", "new-pass"))9596 97 98 assertEquals(slotABefore, storage.slotA)99 assertEquals(slotBBefore, storage.slotB)100 101 repo.lock()102 assertEquals(UnlockResult.Success, repo.unlock("old-pass"))103 assertEquals(1, repo.getEntries().size)104 assertTrue(repo.getEntries().any { it.title == "Keep me" })105 assertEquals(UnlockResult.WrongPassword, repo.unlock("new-pass"))106 }107108 @Test fun change_password_happy_path_swaps_session_key_and_disk() = runBlocking {109 val storage = FaultStorage()110 val repo = repoWithPassword(storage, "old-pass")111 val slotABefore = storage.slotA!!112113 assertTrue(repo.changePassword("old-pass", "new-pass"))114115 116 assertNotEquals(slotABefore.salt, storage.slotA?.salt)117 assertNotEquals(slotABefore.blob, storage.slotA?.blob)118 repo.lock()119 assertEquals(UnlockResult.WrongPassword, repo.unlock("old-pass"))120 assertEquals(UnlockResult.Success, repo.unlock("new-pass"))121 assertTrue(repo.getEntries().any { it.title == "Keep me" })122 }123124 125126 @Test fun reset_password_with_slot_b_failure_restores_prior_slot_a() = runBlocking {127 val storage = FaultStorage()128 val repo = repoWithPassword(storage, "first-pass")129 val words = recoveryWordsOf(repo)130 repo.lock()131132 assertTrue(repo.unlockWithRecoveryWords(words))133 val slotABefore = storage.slotA134 val slotBBefore = storage.slotB135 storage.failWriteSlotB = true136137 assertFalse(repo.resetPasswordWithRecoveryWords(words, "second-pass"))138139 140 141 assertEquals(slotABefore, storage.slotA)142 assertEquals(slotBBefore, storage.slotB)143 144 145 storage.failWriteSlotB = false146 assertTrue(repo.resetPasswordWithRecoveryWords(words, "second-pass"))147 repo.lock()148 assertEquals(UnlockResult.Success, repo.unlock("second-pass"))149 assertTrue(repo.getEntries().any { it.title == "Keep me" })150 }151152 @Test fun reset_password_with_stamp_failure_restores_both_slots() = runBlocking {153 val storage = FaultStorage()154 val repo = repoWithPassword(storage, "first-pass")155 val words = recoveryWordsOf(repo)156 repo.lock()157158 assertTrue(repo.unlockWithRecoveryWords(words))159 val slotABefore = storage.slotA160 val slotBBefore = storage.slotB161 storage.failStamp = true162163 assertFalse(repo.resetPasswordWithRecoveryWords(words, "second-pass"))164165 166 167 assertEquals(slotABefore, storage.slotA)168 assertEquals(slotBBefore, storage.slotB)169 170 171 assertTrue(repo.getEntries().any { it.title == "Keep me" })172 173 storage.failStamp = false174 assertTrue(repo.resetPasswordWithRecoveryWords(words, "second-pass"))175 repo.lock()176 assertEquals(UnlockResult.Success, repo.unlock("second-pass"))177 }178}