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 63 64 65 val openChar = Regex("\"a\"\\s*:\\s*\\{").find(wrapper)!!.range.last66 val open = openChar + 167 var depth = 168 var end = open69 while (end < wrapper.length && depth > 0) {70 when (wrapper[end]) {71 '{' -> depth++72 '}' -> depth--73 }74 if (depth == 0) break75 end++76 }77 val slotA = wrapper.substring(openChar, end + 1)78 val browserWrapper = buildJsonObject {79 put("a", JsonPrimitive(slotA))80 put("b", JsonNull)81 }.toString()82 return Base64.getUrlEncoder().withoutPadding().encodeToString(browserWrapper.toByteArray(Charsets.UTF_8))83 }8485 86 private fun existingVault(): Pair<VaultRepository, FaultStorage> = runBlocking {87 val storage = FaultStorage()88 val repo = VaultRepository(storage)89 repo.createVault("local pass")90 repo.addEntry(VaultEntry(id = "", title = "Local", type = "login", password = "p", createdAt = 0, modifiedAt = 0))91 repo.lock()92 repo to storage93 }9495 private fun assertLocalVaultIntact(repo: VaultRepository, storage: FaultStorage, before: StoredVault?) =96 runBlocking {97 assertEquals("slot A must be untouched", before, storage.slotA)98 assertEquals(UnlockResult.Success, repo.unlock("local pass"))99 assertTrue(repo.getEntries().any { it.title == "Local" })100 }101102 @Test fun wrong_password_on_android_shaped_codex_preserves_local_vault() {103 val blob = sourceCodexBlob()104 val (repo, storage) = existingVault()105 val before = storage.slotA106 runBlocking { assertEquals(UnlockResult.WrongPassword, repo.restoreFromCodex(blob, "not the pass")) }107 assertLocalVaultIntact(repo, storage, before)108 }109110 @Test fun wrong_password_on_browser_shaped_codex_preserves_local_vault() {111 val blob = asBrowserShapedWrapper(sourceCodexBlob())112 val (repo, storage) = existingVault()113 val before = storage.slotA114 runBlocking { assertEquals(UnlockResult.WrongPassword, repo.restoreFromCodex(blob, "not the pass")) }115 assertLocalVaultIntact(repo, storage, before)116 }117118 @Test fun slot_a_write_failure_preserves_local_vault() {119 val blob = sourceCodexBlob()120 val (repo, storage) = existingVault()121 val before = storage.slotA122 storage.failSlotA = true123 runBlocking {124 assertEquals(UnlockResult.Error("storage_write_failed"), repo.restoreFromCodex(blob, "source pass"))125 }126 storage.failSlotA = false127 assertLocalVaultIntact(repo, storage, before)128 }129130 @Test fun later_write_failure_rolls_back_to_local_vault() {131 val blob = sourceCodexBlob()132 val (repo, storage) = existingVault()133 val before = storage.slotA134 storage.failSlotB = true135 runBlocking {136 assertEquals(UnlockResult.Error("storage_write_failed"), repo.restoreFromCodex(blob, "source pass"))137 }138 storage.failSlotB = false139 assertLocalVaultIntact(repo, storage, before)140 }141}