VaultKeyRotationHooksTest.kt

3.0 KB · raw

1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test78/**9 * PB-26-126: the PIN and biometric escrows wrap a copy of the slot-A key, so10 * every rotation site must fire the invalidation signal. Before this, only11 * changePassword cleared anything — and it cleared the PIN only, leaving a12 * biometric wrapper the hardware would still accept.13 */14class VaultKeyRotationHooksTest {1516    private class MemStorage : VaultStorage {17        var slotA: StoredVault? = null18        var slotB: String? = null19        private var ver: Long = 020        private var settings = VaultSettings()21        override fun loadSlotA() = slotA22        override fun hasVault() = slotA != null23        override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }24        override fun loadSlotB() = slotB25        override fun writeSlotB(blobBase64: String): Boolean { slotB = blobBase64; return true }26        var decoy: StoredVault? = null27        override fun loadDecoySlot() = decoy28        override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }29        override fun hasDecoySlot() = decoy != null30        override fun clearDecoySlot() { decoy = null }31        override fun version() = ver32        override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }33        override fun loadSettings() = settings34        override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }35        var history = VaultHistory()36        override fun loadVaultHistory() = history37        override fun writeVaultHistory(h: VaultHistory): Boolean { history = h; return true }38        override fun clearVaultHistory() { history = VaultHistory() }39        override fun clearVault(): Boolean { slotA = null; slotB = null; decoy = null; ver = 040        settings = VaultSettings(); history = VaultHistory(); return true }41    }4243    @Test fun every_rotation_site_fires_the_escrow_signal() = runBlocking {44        val repo = VaultRepository(MemStorage())45        var firings = 046        VaultKeyRotationHooks.onRotated = { firings++ }47        try {48            assertTrue(repo.createVault("first-password") is CreateVaultResult.Success)49            assertEquals("creating a vault is not a rotation", 0, firings)5051            assertTrue(repo.changePassword("first-password", "second-password"))52            assertEquals("changePassword must invalidate both escrows", 1, firings)5354            // A checksum-invalid phrase still rotates the wrapping key (the prior55            // escrows are carried forward), which is exactly the case this hook56            // exists for.57            val words = List(12) { "not-a-real-word" }58            assertTrue(repo.resetPasswordWithRecoveryWords(words, "third-password"))59            assertEquals("recovery reset must invalidate both escrows", 2, firings)60        } finally {61            VaultKeyRotationHooks.onRotated = {}62        }63    }64}