CodexPdfRoundTripTest.kt

6.6 KB · raw

1package com.peachpasswords.codex23import androidx.test.ext.junit.runners.AndroidJUnit44import androidx.test.platform.app.InstrumentationRegistry5import com.peachpasswords.vault.CreateVaultResult6import com.peachpasswords.vault.PasskeyData7import com.peachpasswords.vault.StoredVault8import com.peachpasswords.vault.UnlockResult9import com.peachpasswords.vault.VaultEntry10import com.peachpasswords.vault.VaultRepository11import com.peachpasswords.vault.VaultSettings12import com.peachpasswords.vault.VaultStorage13import java.io.File14import java.io.FileOutputStream15import kotlinx.coroutines.runBlocking16import org.junit.Assert.assertEquals17import org.junit.Assert.assertTrue18import org.junit.Test19import org.junit.runner.RunWith2021@RunWith(AndroidJUnit4::class)22class CodexPdfRoundTripTest {2324    @Test25    fun diversified250EntryVaultRoundTripsThroughProductionPdf() = runBlocking {26        val source = VaultRepository(MemoryStorage())27        assertEquals(CreateVaultResult.Success, source.createVault(MASTER_PASSWORD))28        diversifiedEntries().forEach { source.addEntry(it) }2930        val expected = source.getEntries()31        assertEquals(250, expected.size)3233        val (pdfBytes, manifest) = CodexGenerator.generate(source, now = 1_700_000_000_000)34        assertEquals(250, manifest.entryCount)35        assertTrue(manifest.stripeCount > 1)3637        val context = InstrumentationRegistry.getInstrumentation().targetContext38        val pdfFile = File(context.cacheDir, "codex_250_entry_roundtrip.pdf")39        FileOutputStream(pdfFile).use { it.write(pdfBytes) }4041        val decoded = CodexRestore.decodeFromUri(context, android.net.Uri.fromFile(pdfFile))42        assertEquals(manifest.totalChunks, decoded.envelopes.size)43        val payload = CodexRestore.reconstructPayload(decoded.envelopes)4445        val restored = VaultRepository(MemoryStorage())46        assertEquals(UnlockResult.Success, restored.restoreFromCodex(payload.vaultBlob, MASTER_PASSWORD))47        val actual = restored.getEntries()4849        assertEquals(mapOf("login" to 63, "note" to 63, "card" to 62, "passkey" to 62), actual.groupingBy { it.type }.eachCount())50        assertEquals(expected.map { it.title }.toSet(), actual.map { it.title }.toSet())51        assertTrue(actual.any { it.password == "pāssword-000-🔐-${"x".repeat(64)}" })52        assertTrue(actual.any { it.notes == "private note 249\n日本語 🌸" })53        assertTrue(actual.any { it.cardNumber == "4000000000000246" && it.cvc == "246" })54        assertTrue(actual.any { it.passkey?.credentialId == "credential-247" && it.passkey.privateKeyJwk?.contains("secret-247") == true })55    }5657    private fun diversifiedEntries(): List<VaultEntry> = List(250) { index ->58        val suffix = index.toString().padStart(3, '0')59        val timestamp = 1_700_000_000_000 + index60        when (index % 4) {61            0 -> VaultEntry(62                id = "login-$suffix", type = "login", title = "Login $suffix — café",63                username = "[email protected]", email = "[email protected]",64                password = "pāssword-$suffix-🔐-${"x".repeat(64 + index % 17)}",65                url = "https://login-$suffix.example.com/account",66                additionalUrls = listOf("https://alt-$suffix.example.net/sign-in"),67                totp = "JBSWY3DPEHPK3PXP", notes = "login note $index\nline two",68                tags = listOf("seed", "login", "group-${index % 7}"),69                createdAt = timestamp, modifiedAt = timestamp,70            )71            1 -> VaultEntry(72                id = "note-$suffix", type = "note", title = "Secure note $suffix",73                notes = "private note $index\n日本語 🌸",74                tags = listOf("seed", "note", "group-${index % 11}"),75                createdAt = timestamp, modifiedAt = timestamp,76            )77            2 -> VaultEntry(78                id = "card-$suffix", type = "card", title = "Travel card $suffix",79                cardholderName = "Card Holder $suffix", cardNumber = "4000000000000$suffix",80                expMonth = ((index % 12) + 1).toString().padStart(2, '0'),81                expYear = (2030 + index % 10).toString(), cvc = suffix,82                cardNickname = "wallet-$suffix", cardNetwork = "visa", notes = "billing profile $index",83                tags = listOf("seed", "card"), createdAt = timestamp, modifiedAt = timestamp,84            )85            else -> VaultEntry(86                id = "passkey-$suffix", type = "passkey", title = "Passkey $suffix",87                username = "passkey-user-$suffix",88                passkey = PasskeyData(89                    credentialId = "credential-$index",90                    publicKeyCose = "cose-public-key-$suffix-${"q".repeat(48 + index % 13)}",91                    privateKeyJwk = """{"kty":"EC","crv":"P-256","x":"x-$suffix","y":"y-$suffix","d":"secret-$index"}""",92                    rpId = "service-$suffix.example.org", rpName = "Service $suffix",93                    userHandle = "handle-$suffix", userName = "passkey-user-$suffix",94                    userDisplayName = "Passkey User $suffix", signCount = index,95                    backupEligible = index % 2 == 0, backupState = index % 3 == 0,96                    transports = listOf("internal", "hybrid"), createdAt = timestamp,97                ),98                tags = listOf("seed", "passkey"), createdAt = timestamp, modifiedAt = timestamp,99            )100        }101    }102103    private class MemoryStorage : VaultStorage {104        private var slotA: StoredVault? = null105        private var slotB: String? = null106        private var decoy: StoredVault? = null107        private var version = 0L108        private var settings = VaultSettings()109110        override fun loadSlotA() = slotA111        override fun hasVault() = slotA != null112        override fun writeSlotA(slot: StoredVault) = true.also { slotA = slot }113        override fun loadSlotB() = slotB114        override fun writeSlotB(blobBase64: String) = true.also { slotB = blobBase64 }115        override fun loadDecoySlot() = decoy116        override fun writeDecoySlot(slot: StoredVault) = true.also { decoy = slot }117        override fun clearDecoySlot() { decoy = null }118        override fun version() = version119        override fun stampLocalMutation() = maxOf(System.currentTimeMillis(), version + 1).also { version = it }120        override fun loadSettings() = settings121        override fun writeSettings(settings: VaultSettings) = true.also { this.settings = settings }122        override fun clearVault() = true.also { slotA = null; slotB = null; decoy = null; version = 0 }123    }124125    private companion object {126        const val MASTER_PASSWORD = "codex-device-seed-password"127    }128}