1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertFalse5import org.junit.Assert.assertNotNull6import org.junit.Assert.assertNull7import org.junit.Assert.assertTrue8import org.junit.Test9101112131415class HiddenSessionStateTest {1617 private class FaultStorage : VaultStorage {18 var slotA: StoredVault? = null19 var slotB: String? = null20 var failWriteSlotA = false21 private var ver: Long = 022 private var settings = VaultSettings()23 override fun loadSlotA() = slotA24 override fun hasVault() = slotA != null25 override fun writeSlotA(slot: StoredVault): Boolean {26 if (failWriteSlotA) return false27 slotA = slot28 return true29 }30 override fun loadSlotB() = slotB31 override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }32 var decoy: StoredVault? = null33 override fun loadDecoySlot() = decoy34 override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }35 override fun hasDecoySlot() = decoy != null36 override fun clearDecoySlot() { decoy = null }37 override fun version() = ver38 override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }39 override fun loadSettings() = settings40 override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }41 override fun clearVault() { slotA = null; slotB = null; ver = 0; settings = VaultSettings() }42 }4344 private fun recoveryWordsOf(repo: VaultRepository): List<String> {45 val key = repo.sessionRootKey()!!46 return try { rootKeyToWords(key) } finally { VaultCrypto.zero(key) }47 }4849 50 51 52 @Test fun failed_escrow_write_restores_prior_recovery_flags() = runBlocking {53 val storage = FaultStorage()54 val repo = VaultRepository(storage)55 repo.createVault("pw")56 val words = recoveryWordsOf(repo)57 assertFalse(repo.isRecoveryWordsSet())5859 storage.failWriteSlotA = true60 assertFalse(repo.setRecoveryWords(words))61 assertFalse("flags must roll back when the escrow never landed", repo.isRecoveryWordsSet())62 assertFalse(repo.verifyRecoveryWords(words))63 assertFalse(repo.hasRecoveryEscrow())6465 66 storage.failWriteSlotA = false67 assertTrue(repo.setRecoveryWords(words))68 assertTrue(repo.isRecoveryWordsSet())69 assertTrue(repo.verifyRecoveryWords(words))70 assertTrue(repo.hasRecoveryEscrow())71 }7273 @Test fun sync_accessors_are_null_when_locked() = runBlocking {74 val repo = VaultRepository(FaultStorage())75 repo.createVault("pw")76 assertNotNull(repo.syncMasterKey())77 assertNotNull(repo.syncVaultId())78 repo.lock()79 assertNull(repo.syncMasterKey())80 assertNull(repo.syncVaultId())81 }8283 84 85 @Test fun sync_accessors_are_null_during_recovery_half_unlock() = runBlocking {86 val storage = FaultStorage()87 val repo = VaultRepository(storage)88 repo.createVault("pw")89 val words = recoveryWordsOf(repo)90 assertTrue(repo.setRecoveryWords(words))91 repo.lock()9293 assertTrue(repo.unlockWithRecoveryWords(words))94 assertFalse(repo.isUnlocked.value)95 assertNull(repo.syncMasterKey())96 assertNull(repo.syncVaultId())97 assertNotNull("the local-only accessor still sees the recovered key", repo.sessionRootKey())9899 100 assertTrue(repo.resetPasswordWithRecoveryWords(words, "new-pw"))101 assertTrue(repo.isUnlocked.value)102 assertNotNull(repo.syncMasterKey())103 assertNotNull(repo.syncVaultId())104 }105106 @Test fun failed_create_retains_no_session_state() = runBlocking {107 val storage = FaultStorage()108 storage.failWriteSlotA = true109 val repo = VaultRepository(storage)110111 assertTrue(repo.createVault("pw") is CreateVaultResult.Error)112 assertFalse(repo.isUnlocked.value)113 assertNull("a failed create must not retain a root key", repo.sessionRootKey())114 assertNull(repo.syncMasterKey())115 assertNull(repo.currentVaultKey())116 }117}