SlotPaddingTest.kt

3.1 KB · raw

1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test78/** Exposes the raw stored slots so the padding lengths (MN-1) can be inspected. */9private class PaddingFakeStorage : VaultStorage {10    var slotA: StoredVault? = null11    var slotBNoise: String? = null12    var decoy: StoredVault? = null13    private var ver: Long = 014    private var settings = VaultSettings()15    override fun loadSlotA() = slotA16    override fun hasVault() = slotA != null17    override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true }18    override fun writeSlotB(blobBase64: String): Boolean { slotBNoise = blobBase64; return true }19    override fun loadDecoySlot() = decoy20    override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true }21    override fun hasDecoySlot() = decoy != null22    override fun clearDecoySlot() { decoy = null }23    override fun version() = ver24    override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver }25    override fun loadSettings() = settings26    override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true }27    override fun clearVault() { slotA = null; slotBNoise = null; ver = 0; settings = VaultSettings() }28}2930class SlotPaddingTest {31    @Test fun payload_round_trips_through_bucket_padding() {32        val payload = VaultPayload(33            entries = listOf(VaultEntry(id = "1", title = "GitHub", type = "login", password = "pw", createdAt = 0, modifiedAt = 0)),34            rootKeyB64 = "AAAA",35        )36        val padded = VaultCodec.encode(payload)37        assertEquals("padded to the smallest bucket", VaultCrypto.PADDING_BUCKETS.first(), padded.size)38        val decoded = VaultCodec.decode(padded)39        assertEquals("padding is transparent to decode", "GitHub", decoded.entries.single().title)40        assertEquals("AAAA", decoded.rootKeyB64)41    }4243    @Test fun legacy_unpadded_blob_still_decodes() {44        // A blob written before MN-1 has no trailing NUL padding — must still decode.45        val raw = "{\"entries\":[],\"rootKeyB64\":\"X\"}".toByteArray(Charsets.UTF_8)46        assertEquals("X", VaultCodec.decode(raw).rootKeyB64)47    }4849    @Test fun real_and_duress_slots_share_a_bucket_for_small_vaults() = runBlocking {50        val storage = PaddingFakeStorage()51        val repo = VaultRepository(storage)52        repo.createVault("the-real-password-123")53        repo.addEntry(VaultEntry(id = "", title = "Bank", type = "login", password = "pw", createdAt = 0, modifiedAt = 0))54        assertTrue(repo.setDuressPassphrase("open-the-decoy-456"))5556        val slotALen = storage.slotA!!.blob.length57        val decoyLen = storage.decoy!!.blob.length58        assertEquals("a small real vault and its decoy quantise to the same slot length", slotALen, decoyLen)59        // And the always-present noise slot matches slot A's length too (±base64 rounding).60        assertTrue("noise slot matches slot A length", kotlin.math.abs(storage.slotBNoise!!.length - slotALen) <= 4)61    }62}