1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import kotlinx.serialization.json.JsonNull5import kotlinx.serialization.json.JsonPrimitive6import kotlinx.serialization.json.buildJsonObject7import org.junit.Assert.assertEquals8import org.junit.Assert.assertTrue9import org.junit.Test10import java.util.Base641112131415161718class CodexRestoreAtomicityTest {1920 private class FaultStorage : VaultStorage {21 var slotA: StoredVault? = null22 var slotB: String? = null23 var failSlotA = false24 var failSlotB = false25 private var ver: Long = 026 private var settings = VaultSettings()27 override fun loadSlotA() = slotA28 override fun hasVault() = slotA != null29 override fun writeSlotA(slot: StoredVault): Boolean {30 if (failSlotA) return false31 slotA = slot32 return true33 }34 override fun loadSlotB() = slotB35 override fun writeSlotB(blobBase64: String): Boolean {36 if (failSlotB) return false37 slotB = blobBase6438 return true39 }40 var decoy: StoredVault? = null41 override fun loadDecoySlot() = decoy42 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }43 override fun hasDecoySlot() = decoy != null44 override fun clearDecoySlot() { decoy = null }45 override fun version() = ver46 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }47 override fun loadSettings() = settings48 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }49 override fun clearVault() { slotA = null; slotB = null; ver = 0; settings = VaultSettings() }50 }5152 private fun sourceCodexBlob(): String = runBlocking {53 val src = VaultRepository(FaultStorage())54 src.createVault("source pass")55 src.addEntry(VaultEntry(id = "", title = "Incoming", type = "login", password = "p", createdAt = 0, modifiedAt = 0))56 src.codexVaultBlobBase64()!!57 }5859 60 private fun asBrowserShapedWrapper(androidBlob: String): String {61 val wrapper = String(Base64.getUrlDecoder().decode(androidBlob), Charsets.UTF_8)62 val slotA = Regex("\"a\"\\s*:\\s*(\\{.*\\})\\s*\\}\\s*$").find(wrapper)!!.groupValues[1]63 val browserWrapper = buildJsonObject {64 put("a", JsonPrimitive(slotA))65 put("b", JsonNull)66 }.toString()67 return Base64.getUrlEncoder().withoutPadding().encodeToString(browserWrapper.toByteArray(Charsets.UTF_8))68 }6970 71 private fun existingVault(): Pair<VaultRepository, FaultStorage> = runBlocking {72 val storage = FaultStorage()73 val repo = VaultRepository(storage)74 repo.createVault("local pass")75 repo.addEntry(VaultEntry(id = "", title = "Local", type = "login", password = "p", createdAt = 0, modifiedAt = 0))76 repo.lock()77 repo to storage78 }7980 private fun assertLocalVaultIntact(repo: VaultRepository, storage: FaultStorage, before: StoredVault?) =81 runBlocking {82 assertEquals("slot A must be untouched", before, storage.slotA)83 assertEquals(UnlockResult.Success, repo.unlock("local pass"))84 assertTrue(repo.getEntries().any { it.title == "Local" })85 }8687 @Test fun wrong_password_on_android_shaped_codex_preserves_local_vault() {88 val blob = sourceCodexBlob()89 val (repo, storage) = existingVault()90 val before = storage.slotA91 runBlocking { assertEquals(UnlockResult.WrongPassword, repo.restoreFromCodex(blob, "not the pass")) }92 assertLocalVaultIntact(repo, storage, before)93 }9495 @Test fun wrong_password_on_browser_shaped_codex_preserves_local_vault() {96 val blob = asBrowserShapedWrapper(sourceCodexBlob())97 val (repo, storage) = existingVault()98 val before = storage.slotA99 runBlocking { assertEquals(UnlockResult.WrongPassword, repo.restoreFromCodex(blob, "not the pass")) }100 assertLocalVaultIntact(repo, storage, before)101 }102103 @Test fun slot_a_write_failure_preserves_local_vault() {104 val blob = sourceCodexBlob()105 val (repo, storage) = existingVault()106 val before = storage.slotA107 storage.failSlotA = true108 runBlocking {109 assertEquals(UnlockResult.Error("storage_write_failed"), repo.restoreFromCodex(blob, "source pass"))110 }111 storage.failSlotA = false112 assertLocalVaultIntact(repo, storage, before)113 }114115 @Test fun later_write_failure_rolls_back_to_local_vault() {116 val blob = sourceCodexBlob()117 val (repo, storage) = existingVault()118 val before = storage.slotA119 storage.failSlotB = true120 runBlocking {121 assertEquals(UnlockResult.Error("storage_write_failed"), repo.restoreFromCodex(blob, "source pass"))122 }123 storage.failSlotB = false124 assertLocalVaultIntact(repo, storage, before)125 }126}