package com.peachpasswords.codex import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry import com.peachpasswords.vault.CreateVaultResult import com.peachpasswords.vault.PasskeyData import com.peachpasswords.vault.StoredVault import com.peachpasswords.vault.UnlockResult import com.peachpasswords.vault.VaultEntry import com.peachpasswords.vault.VaultRepository import com.peachpasswords.vault.VaultSettings import com.peachpasswords.vault.VaultStorage import java.io.File import java.io.FileOutputStream import kotlinx.coroutines.runBlocking import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Test import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class CodexPdfRoundTripTest { @Test fun diversified250EntryVaultRoundTripsThroughProductionPdf() = runBlocking { val source = VaultRepository(MemoryStorage()) assertEquals(CreateVaultResult.Success, source.createVault(MASTER_PASSWORD)) diversifiedEntries().forEach { source.addEntry(it) } val expected = source.getEntries() assertEquals(250, expected.size) val (pdfBytes, manifest) = CodexGenerator.generate(source, now = 1_700_000_000_000) assertEquals(250, manifest.entryCount) assertTrue(manifest.stripeCount > 1) val context = InstrumentationRegistry.getInstrumentation().targetContext val pdfFile = File(context.cacheDir, "codex_250_entry_roundtrip.pdf") FileOutputStream(pdfFile).use { it.write(pdfBytes) } val decoded = CodexRestore.decodeFromUri(context, android.net.Uri.fromFile(pdfFile)) assertEquals(manifest.totalChunks, decoded.envelopes.size) val payload = CodexRestore.reconstructPayload(decoded.envelopes) val restored = VaultRepository(MemoryStorage()) assertEquals(UnlockResult.Success, restored.restoreFromCodex(payload.vaultBlob, MASTER_PASSWORD)) val actual = restored.getEntries() assertEquals(mapOf("login" to 63, "note" to 63, "card" to 62, "passkey" to 62), actual.groupingBy { it.type }.eachCount()) assertEquals(expected.map { it.title }.toSet(), actual.map { it.title }.toSet()) assertTrue(actual.any { it.password == "pāssword-000-🔐-${"x".repeat(64)}" }) assertTrue(actual.any { it.notes == "private note 249\n日本語 🌸" }) assertTrue(actual.any { it.cardNumber == "4000000000000246" && it.cvc == "246" }) assertTrue(actual.any { it.passkey?.credentialId == "credential-247" && it.passkey.privateKeyJwk?.contains("secret-247") == true }) } private fun diversifiedEntries(): List = List(250) { index -> val suffix = index.toString().padStart(3, '0') val timestamp = 1_700_000_000_000 + index when (index % 4) { 0 -> VaultEntry( id = "login-$suffix", type = "login", title = "Login $suffix — café", username = "user+$suffix@example.com", email = "recovery+$suffix@example.net", password = "pāssword-$suffix-🔐-${"x".repeat(64 + index % 17)}", url = "https://login-$suffix.example.com/account", additionalUrls = listOf("https://alt-$suffix.example.net/sign-in"), totp = "JBSWY3DPEHPK3PXP", notes = "login note $index\nline two", tags = listOf("seed", "login", "group-${index % 7}"), createdAt = timestamp, modifiedAt = timestamp, ) 1 -> VaultEntry( id = "note-$suffix", type = "note", title = "Secure note $suffix", notes = "private note $index\n日本語 🌸", tags = listOf("seed", "note", "group-${index % 11}"), createdAt = timestamp, modifiedAt = timestamp, ) 2 -> VaultEntry( id = "card-$suffix", type = "card", title = "Travel card $suffix", cardholderName = "Card Holder $suffix", cardNumber = "4000000000000$suffix", expMonth = ((index % 12) + 1).toString().padStart(2, '0'), expYear = (2030 + index % 10).toString(), cvc = suffix, cardNickname = "wallet-$suffix", cardNetwork = "visa", notes = "billing profile $index", tags = listOf("seed", "card"), createdAt = timestamp, modifiedAt = timestamp, ) else -> VaultEntry( id = "passkey-$suffix", type = "passkey", title = "Passkey $suffix", username = "passkey-user-$suffix", passkey = PasskeyData( credentialId = "credential-$index", publicKeyCose = "cose-public-key-$suffix-${"q".repeat(48 + index % 13)}", privateKeyJwk = """{"kty":"EC","crv":"P-256","x":"x-$suffix","y":"y-$suffix","d":"secret-$index"}""", rpId = "service-$suffix.example.org", rpName = "Service $suffix", userHandle = "handle-$suffix", userName = "passkey-user-$suffix", userDisplayName = "Passkey User $suffix", signCount = index, backupEligible = index % 2 == 0, backupState = index % 3 == 0, transports = listOf("internal", "hybrid"), createdAt = timestamp, ), tags = listOf("seed", "passkey"), createdAt = timestamp, modifiedAt = timestamp, ) } } private class MemoryStorage : VaultStorage { private var slotA: StoredVault? = null private var slotB: String? = null private var decoy: StoredVault? = null private var version = 0L private var settings = VaultSettings() override fun loadSlotA() = slotA override fun hasVault() = slotA != null override fun writeSlotA(slot: StoredVault) = true.also { slotA = slot } override fun loadSlotB() = slotB override fun writeSlotB(blobBase64: String) = true.also { slotB = blobBase64 } override fun loadDecoySlot() = decoy override fun writeDecoySlot(slot: StoredVault) = true.also { decoy = slot } override fun clearDecoySlot() { decoy = null } override fun version() = version override fun stampLocalMutation() = maxOf(System.currentTimeMillis(), version + 1).also { version = it } override fun loadSettings() = settings override fun writeSettings(settings: VaultSettings) = true.also { this.settings = settings } override fun clearVault() = true.also { slotA = null; slotB = null; decoy = null; version = 0 } } private companion object { const val MASTER_PASSWORD = "codex-device-seed-password" } }