package com.peachpasswords.vault import org.junit.Assert.assertEquals import org.junit.Assert.assertNotNull import org.junit.Assert.assertNull import org.junit.Assert.assertThrows import org.junit.Assert.assertTrue import org.junit.Test import java.util.Base64 import kotlinx.serialization.json.Json import kotlinx.serialization.json.contentOrNull import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive class PeachScriptTest { @Test fun parses_shared_v4_long_password_vector() { val vector = Json.parseToJsonElement( checkNotNull(javaClass.classLoader?.getResource("fixtures/golden/peach-payload-v4.json")).readText(), ).jsonObject val passwords = vector.getValue("passwords").jsonObject val envelope = Base64.getDecoder().decode(vector.getValue("envelopeBase64").jsonPrimitive.content) val entries = PeachPayloadCodec.parsePayload(envelope, vector.getValue("passphrase").jsonPrimitive.content) assertEquals(passwords.getValue("ascii").jsonPrimitive.content, entries[0].password) assertEquals(passwords.getValue("unicode").jsonPrimitive.content, entries[1].password) assertEquals(256, entries[0].password?.length) assertEquals(256, entries[1].password?.length) } private fun login() = VaultEntry( id = "11111111-2222-3333-4444-555555555555", type = "login", title = "GitHub", url = "https://github.com", username = "june", email = "june@example.com", password = "hunter2-ฮฃ-๐Ÿ”", totp = "JBSWY3DPEHPK3PXP", tags = listOf("dev", "work"), notes = "primary account", additionalUrls = listOf("https://gist.github.com"), breach = 3, passwordChangedAt = 1_600_000_000_000, canary = Canary(webhookUrl = "https://canary.example.com/hook", label = "tripwire"), createdAt = 1_500_000_000_000, modifiedAt = 1_600_000_500_000, ) private fun note() = VaultEntry( id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", type = "note", title = "ใใตใจ ใƒกใƒข ๐ŸŒธ", notes = "ๆ—ฅๆœฌ่ชžใฎใƒŽใƒผใƒˆ\nsecond line", tags = listOf("personal"), createdAt = 1_500_000_000_000, modifiedAt = 1_500_000_000_000, ) private fun card() = VaultEntry( id = "99999999-8888-7777-6666-555544443333", type = "card", title = "Visa", cardholderName = "JUNE MOON", cardNumber = "4111111111111111", expMonth = "04", expYear = "2030", cvc = "123", cardNickname = "daily", cardNetwork = "visa", notes = "spending card", tags = listOf("finance"), createdAt = 1_500_000_000_000, modifiedAt = 1_500_000_000_000, ) // โ”€โ”€ KAT: parse a REAL browser-produced PeachScript payload โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Envelope generated by the browser's own buildPayload (/tmp/gen_peach3.mjs // via vite SSR), proving Android decrypts + zstd-decompresses + unpacks the // exact bytes the extension writes. This is the cross-platform import proof. @Test fun parses_browser_produced_payload() { val envelope = Base64.getDecoder().decode(BROWSER_FIXTURE_B64) val entries = PeachPayloadCodec.parsePayload(envelope, "backup-pass-123") assertEquals(3, entries.size) val l = entries[0] assertEquals("11111111-2222-3333-4444-555555555555", l.id) assertEquals("login", l.type) assertEquals("GitHub", l.title) assertEquals("https://github.com", l.url) assertEquals("june", l.username) assertEquals("june@example.com", l.email) assertEquals("hunter2-ฮฃ-๐Ÿ”", l.password) assertEquals("JBSWY3DPEHPK3PXP", l.totp) assertEquals(listOf("dev", "work"), l.tags) assertEquals("primary account", l.notes) assertEquals(listOf("https://gist.github.com"), l.additionalUrls) assertEquals(3, l.breach) assertEquals(1_600_000_000_000L, l.passwordChangedAt) assertEquals("https://canary.example.com/hook", l.canary?.webhookUrl) assertEquals("tripwire", l.canary?.label) assertEquals(1_500_000_000_000L, l.createdAt) assertEquals(1_600_000_500_000L, l.modifiedAt) val n = entries[1] assertEquals("note", n.type) assertEquals("ใใตใจ ใƒกใƒข ๐ŸŒธ", n.title) assertEquals("ๆ—ฅๆœฌ่ชžใฎใƒŽใƒผใƒˆ\nsecond line", n.notes) assertEquals(listOf("personal"), n.tags) val c = entries[2] assertEquals("card", c.type) assertEquals("JUNE MOON", c.cardholderName) assertEquals("4111111111111111", c.cardNumber) assertEquals("04", c.expMonth) assertEquals("2030", c.expYear) assertEquals("123", c.cvc) assertEquals("daily", c.cardNickname) assertEquals("visa", c.cardNetwork) assertEquals("spending card", c.notes) assertEquals(listOf("finance"), c.tags) } // โ”€โ”€ Android self round-trip: all fields, all entry types โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @Test fun self_round_trips_all_fields() { val input = listOf(login(), note(), card()) val out = PeachPayloadCodec.buildPayload(input, "pw-123") val back = PeachPayloadCodec.parsePayload(out, "pw-123") assertEquals(3, back.size) val l = back[0] assertEquals(login().id, l.id) assertEquals("hunter2-ฮฃ-๐Ÿ”", l.password) assertEquals(listOf("dev", "work"), l.tags) assertEquals(3, l.breach) assertEquals(1_600_000_000_000L, l.passwordChangedAt) assertEquals("tripwire", l.canary?.label) assertEquals(listOf("https://gist.github.com"), l.additionalUrls) assertEquals(1_500_000_000_000L, l.createdAt) assertEquals(1_600_000_500_000L, l.modifiedAt) val c = back[2] assertEquals("4111111111111111", c.cardNumber) assertEquals("visa", c.cardNetwork) assertEquals("123", c.cvc) } @Test fun v4_round_trips_256_character_ascii_and_multibyte_passwords() { for (password in listOf("a".repeat(256), "็•Œ".repeat(256))) { val encoded = PeachPayloadCodec.buildPayload(listOf(login().copy(password = password)), "pw-123") val decoded = PeachPayloadCodec.parsePayload(encoded, "pw-123").single() assertEquals(password, decoded.password) } } // โ”€โ”€ Cross-direction: Android export parsed by the browser's algorithm โ”€โ”€โ”€ // (Verified manually via /tmp; here we at least prove a browser-shaped // envelope layout: salt(16)+nonce(12)+ct+tag, padded to a size bucket.) @Test fun export_envelope_has_browser_shape() { val out = PeachPayloadCodec.buildPayload(listOf(login()), "pw") // salt(16)+nonce(12)+tag(16) overhead; payload padded to >= 1024 bucket, // so the ciphertext alone is at least one bucket. assertTrue(out.size >= 16 + 12 + 1024 + 16) } @Test fun empty_vault_round_trips() { val out = PeachPayloadCodec.buildPayload(emptyList(), "pw") assertEquals(0, PeachPayloadCodec.parsePayload(out, "pw").size) } @Test fun unicode_fields_round_trip() { val out = PeachPayloadCodec.buildPayload(listOf(note()), "pw") val n = PeachPayloadCodec.parsePayload(out, "pw").single() assertEquals("ใใตใจ ใƒกใƒข ๐ŸŒธ", n.title) assertEquals("ๆ—ฅๆœฌ่ชžใฎใƒŽใƒผใƒˆ\nsecond line", n.notes) } @Test fun wrong_password_is_invalid_passphrase() { val out = PeachPayloadCodec.buildPayload(listOf(login()), "right-pw") val ex = assertThrows(PeachPayloadCodec.PeachScriptException::class.java) { PeachPayloadCodec.parsePayload(out, "wrong-pw") } assertEquals("INVALID_PASSPHRASE", ex.message) } @Test fun pua_codec_round_trips() { val bytes = ByteArray(256) { it.toByte() } val text = PeachScript.encode(bytes) assertTrue(PeachScript.isPeachScript(text)) assertEquals(256, text.codePointCount(0, text.length)) assertTrue(bytes.contentEquals(PeachScript.decode(text))) // Whitespace is stripped before decode. assertTrue(bytes.contentEquals(PeachScript.decode(PeachScript.normalise(" $text \n")))) } @Test fun passkey_core_fields_round_trip() { val privateKeyJwk = """{"kty":"EC","crv":"P-256","x":"x1","y":"y1","d":"d1"}""" val pk = VaultEntry( id = "12121212-3434-5656-7878-909090909090", type = "passkey", title = "Passkey", passkey = PasskeyData( credentialId = "cred-abc", publicKeyCose = "cose-xyz", rpId = "example.com", privateKeyJwk = privateKeyJwk, rpName = "Example", userHandle = "uh-1", userName = "june", userDisplayName = "June", ), createdAt = 1_500_000_000_000, modifiedAt = 1_500_000_000_000, ) val out = PeachPayloadCodec.buildPayload(listOf(pk), "pw") val back = PeachPayloadCodec.parsePayload(out, "pw").single() assertEquals("passkey", back.type) assertNotNull(back.passkey) assertEquals("cred-abc", back.passkey?.credentialId) assertEquals("example.com", back.passkey?.rpId) assertEquals("june", back.passkey?.userName) val backJwk = Json.parseToJsonElement(requireNotNull(back.passkey?.privateKeyJwk)).jsonObject assertEquals("EC", backJwk["kty"]?.jsonPrimitive?.contentOrNull) assertEquals("d1", backJwk["d"]?.jsonPrimitive?.contentOrNull) } @Test fun full_export_import_via_peachscript_text() { val text = com.peachpasswords.ui.vault.PeachScriptExport.buildEncrypted(listOf(login(), card()), "bp") assertTrue(com.peachpasswords.ui.vault.PeachScriptImport.looksLikePeachScript(text)) val r = com.peachpasswords.ui.vault.PeachScriptImport.decryptAndParse(text, "bp") assertTrue(r is com.peachpasswords.ui.vault.PeachScriptImport.DecryptResult.Success) r as com.peachpasswords.ui.vault.PeachScriptImport.DecryptResult.Success assertEquals(2, r.entries.size) assertEquals("hunter2-ฮฃ-๐Ÿ”", r.entries[0].password) assertNull( (com.peachpasswords.ui.vault.PeachScriptImport.decryptAndParse(text, "nope") as? com.peachpasswords.ui.vault.PeachScriptImport.DecryptResult.Success)?.entries, ) } companion object { // Real browser PeachScript envelope (base64) โ€” see /tmp/gen_peach3.mjs. private const val BROWSER_FIXTURE_B64 = "RTRXPzyl+oHgPhKUK3DCdab09Rb+zMF5Dfphcydo2qzHWqE2PfJHdLEalnhL7jSOxCiowSjIYmrbkCLP2qCLeSHwceeSD9RhBx04bHNoc5J/ml/sXrY/7BFIZizNk5dlDt921VSQNhaNEzYmP/fKkYWjFpz+OOhUCBTUgt+WtCwa5mf8Ctf6R1UPkh21Fu3pwwb/IQjhFvij+CYgS+1skXtogw0iYTrNbSLMIxMsIKvXytFGOp4IGWbbATO/7vFPXnwc2Zh9gOkbUbVM8D7dBUBV+hTodCNHO8PsvxzPAQL0io4/t/Iu60x91eiB8+TTJEwQy9d/aNGM0hgAZq5TzBj4IpYW2tGwfMqRwhuQz07e7SrcL3ggJVlrOf+rph0UnzgdRuCeMS67/83IhNg0UUCftSqdBdQFeqE3UNLavRohLO4AaqTLR1i4UfDApjL79MVtqjztfj8xTJr+zf1SH0BZ0/6PCyIXnm526lxk4WALgJiXu1FWoIXP3xGjs/ZAe1t7RONiVak51kTLs7djQMh6WLMGxW8KVaFAu1a+jG3Zus9Ynn9FBpowipW1E4rnUnytqUzh/udY0q4Y8nJvavyD3hO5xEbHkslfbPSYqxrrGaL3OFb9llKoLyZSiYGB5p+/QM7xn5DFZ3VOtx90xf9wVEe2Uhfhq0XfQUqT384kWp8Ujlyq4pFB6nIe3f3e8C2pE+fJMou0Q2RFvotx/F56T3Ul2d0fpoN7ucyqJC94Fo6B8Peh1u5oxu5njPZEjRSUl2eBt3K8Aval7d9yYbm7I1ecUSLNJ+nHphL/DMYKL2CYQd90pdyfOeDgRCt4WML8z6hjTtW6uPUKDkd5WVu0qp2ktuzawolWi7aah1BnTZMwH2/jm6bPlLNE5r/bFwr1MAgNscjpwwa0DXTfMovbgJHLxEp6/0vpxuYcCfoRoinpTErt7Eafz2rQqw7mEit3eoUZ1Nv+AdfvBC3PN0pB/L5gdzi/cCrEtVCX2A/1NkhV91EKnx0tCQnkjZyovd9ZhKb/QNgzA6v7lugua4e0foRJJBSqwcJJ4Sr7CUXsK5/yuwXsYcJ0I+yTwiHoe4WA5I/go61xvSPsDV/5aXyS0bJbAa5DusVd/SnUM1QFFwHjOfwGUlU/PupZSr2zvZdds+ZX8B8u16L55gZE8xOdOUq7tAd25Gd3v0r6sSwcYu1amjhbfQlgw2RBQBWHmMZ8Jn715A9RblOMW95EqVu8AhG1Jfl547WdahfgC93Aw3nlT39sUBpzqRwa54N6Q9RHc81uC1q+xtU7CiTIMWJm1qqnVjP89MWMnRpYjRXOYBOop73uHh4BjomUlEzXEXmtJiaMq1nsqW85k+FKnO0bF/rGVzmFvHWuBx4cWYic9e8tmUalxdJxDsqFr+dh3h3Fp/YD9oOIT2XD" } }