1package com.peachpasswords.vault23import kotlinx.coroutines.runBlocking4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test789private 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 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 60 assertTrue("noise slot matches slot A length", kotlin.math.abs(storage.slotBNoise!!.length - slotALen) <= 4)61 }62}