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}