package com.peachpasswords.vault import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test /** Exposes the raw stored slots so the padding lengths (MN-1) can be inspected. */ private class PaddingFakeStorage : VaultStorage { var slotA: StoredVault? = null var slotBNoise: String? = null var decoy: StoredVault? = null private var ver: Long = 0 private var settings = VaultSettings() override fun loadSlotA() = slotA override fun hasVault() = slotA != null override fun writeSlotA(slot: StoredVault): Boolean { slotA = slot; return true } override fun writeSlotB(blobBase64: String): Boolean { slotBNoise = blobBase64; return true } override fun loadDecoySlot() = decoy override fun writeDecoySlot(slot: StoredVault): Boolean { decoy = slot; return true } override fun hasDecoySlot() = decoy != null override fun clearDecoySlot() { decoy = null } override fun version() = ver override fun stampLocalMutation(): Long { ver = maxOf(System.currentTimeMillis(), ver + 1); return ver } override fun loadSettings() = settings override fun writeSettings(s: VaultSettings): Boolean { settings = s; return true } override fun clearVault() { slotA = null; slotBNoise = null; ver = 0; settings = VaultSettings() } } class SlotPaddingTest { @Test fun payload_round_trips_through_bucket_padding() { val payload = VaultPayload( entries = listOf(VaultEntry(id = "1", title = "GitHub", type = "login", password = "pw", createdAt = 0, modifiedAt = 0)), rootKeyB64 = "AAAA", ) val padded = VaultCodec.encode(payload) assertEquals("padded to the smallest bucket", VaultCrypto.PADDING_BUCKETS.first(), padded.size) val decoded = VaultCodec.decode(padded) assertEquals("padding is transparent to decode", "GitHub", decoded.entries.single().title) assertEquals("AAAA", decoded.rootKeyB64) } @Test fun legacy_unpadded_blob_still_decodes() { // A blob written before MN-1 has no trailing NUL padding — must still decode. val raw = "{\"entries\":[],\"rootKeyB64\":\"X\"}".toByteArray(Charsets.UTF_8) assertEquals("X", VaultCodec.decode(raw).rootKeyB64) } @Test fun real_and_duress_slots_share_a_bucket_for_small_vaults() = runBlocking { val storage = PaddingFakeStorage() val repo = VaultRepository(storage) repo.createVault("the-real-password-123") repo.addEntry(VaultEntry(id = "", title = "Bank", type = "login", password = "pw", createdAt = 0, modifiedAt = 0)) assertTrue(repo.setDuressPassphrase("open-the-decoy-456")) val slotALen = storage.slotA!!.blob.length val decoyLen = storage.decoy!!.blob.length assertEquals("a small real vault and its decoy quantise to the same slot length", slotALen, decoyLen) // And the always-present noise slot matches slot A's length too (±base64 rounding). assertTrue("noise slot matches slot A length", kotlin.math.abs(storage.slotBNoise!!.length - slotALen) <= 4) } }