HiddenSessionStateTest.kt

4.8 KB · raw

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.Test910/**11 * M-04: no operation may leave in-memory session state that disagrees with what12 * is on disk, and the sync-key accessors must fail closed whenever the session13 * is not a fully-unlocked real vault.14 */15class 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    // A failed escrow write must not leave the session claiming words are set:50    // the recovery screen would stop offering the confirm step against a slot A51    // that holds no escrow at all.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        // And the retry still works once storage recovers.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    // A recovery unlock installs a root key but no password key — the session is84    // not unlocked yet, so nothing may leave the device under that identity.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        // Finishing the reset unlocks the session and the sync identity returns.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}