1package com.peachpasswords.vault23import org.junit.Assert.assertEquals4import org.junit.Assert.assertNotNull5import org.junit.Assert.assertNull6import org.junit.Assert.assertThrows7import org.junit.Assert.assertTrue8import org.junit.Test9import java.util.Base6410import kotlinx.serialization.json.Json11import kotlinx.serialization.json.contentOrNull12import kotlinx.serialization.json.jsonObject13import kotlinx.serialization.json.jsonPrimitive1415class PeachScriptTest {1617 @Test fun parses_shared_v4_long_password_vector() {18 val vector = Json.parseToJsonElement(19 checkNotNull(javaClass.classLoader?.getResource("fixtures/golden/peach-payload-v4.json")).readText(),20 ).jsonObject21 val passwords = vector.getValue("passwords").jsonObject22 val envelope = Base64.getDecoder().decode(vector.getValue("envelopeBase64").jsonPrimitive.content)23 val entries = PeachPayloadCodec.parsePayload(envelope, vector.getValue("passphrase").jsonPrimitive.content)2425 assertEquals(passwords.getValue("ascii").jsonPrimitive.content, entries[0].password)26 assertEquals(passwords.getValue("unicode").jsonPrimitive.content, entries[1].password)27 assertEquals(256, entries[0].password?.length)28 assertEquals(256, entries[1].password?.length)29 }3031 private fun login() = VaultEntry(32 id = "11111111-2222-3333-4444-555555555555", type = "login", title = "GitHub",33 url = "https://github.com", username = "june", email = "[email protected]",34 password = "hunter2-Σ-🔐", totp = "JBSWY3DPEHPK3PXP", tags = listOf("dev", "work"),35 notes = "primary account", additionalUrls = listOf("https://gist.github.com"),36 breach = 3, passwordChangedAt = 1_600_000_000_000,37 canary = Canary(webhookUrl = "https://canary.example.com/hook", label = "tripwire"),38 createdAt = 1_500_000_000_000, modifiedAt = 1_600_000_500_000,39 )4041 private fun note() = VaultEntry(42 id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", type = "note", title = "そふと メモ 🌸",43 notes = "日本語のノート\nsecond line", tags = listOf("personal"),44 createdAt = 1_500_000_000_000, modifiedAt = 1_500_000_000_000,45 )4647 private fun card() = VaultEntry(48 id = "99999999-8888-7777-6666-555544443333", type = "card", title = "Visa",49 cardholderName = "JUNE MOON", cardNumber = "4111111111111111", expMonth = "04",50 expYear = "2030", cvc = "123", cardNickname = "daily", cardNetwork = "visa",51 notes = "spending card", tags = listOf("finance"),52 createdAt = 1_500_000_000_000, modifiedAt = 1_500_000_000_000,53 )5455 56 57 58 59 @Test fun parses_browser_produced_payload() {60 val envelope = Base64.getDecoder().decode(BROWSER_FIXTURE_B64)61 val entries = PeachPayloadCodec.parsePayload(envelope, "backup-pass-123")62 assertEquals(3, entries.size)6364 val l = entries[0]65 assertEquals("11111111-2222-3333-4444-555555555555", l.id)66 assertEquals("login", l.type)67 assertEquals("GitHub", l.title)68 assertEquals("https://github.com", l.url)69 assertEquals("june", l.username)70 assertEquals("[email protected]", l.email)71 assertEquals("hunter2-Σ-🔐", l.password)72 assertEquals("JBSWY3DPEHPK3PXP", l.totp)73 assertEquals(listOf("dev", "work"), l.tags)74 assertEquals("primary account", l.notes)75 assertEquals(listOf("https://gist.github.com"), l.additionalUrls)76 assertEquals(3, l.breach)77 assertEquals(1_600_000_000_000L, l.passwordChangedAt)78 assertEquals("https://canary.example.com/hook", l.canary?.webhookUrl)79 assertEquals("tripwire", l.canary?.label)80 assertEquals(1_500_000_000_000L, l.createdAt)81 assertEquals(1_600_000_500_000L, l.modifiedAt)8283 val n = entries[1]84 assertEquals("note", n.type)85 assertEquals("そふと メモ 🌸", n.title)86 assertEquals("日本語のノート\nsecond line", n.notes)87 assertEquals(listOf("personal"), n.tags)8889 val c = entries[2]90 assertEquals("card", c.type)91 assertEquals("JUNE MOON", c.cardholderName)92 assertEquals("4111111111111111", c.cardNumber)93 assertEquals("04", c.expMonth)94 assertEquals("2030", c.expYear)95 assertEquals("123", c.cvc)96 assertEquals("daily", c.cardNickname)97 assertEquals("visa", c.cardNetwork)98 assertEquals("spending card", c.notes)99 assertEquals(listOf("finance"), c.tags)100 }101102 103 @Test fun self_round_trips_all_fields() {104 val input = listOf(login(), note(), card())105 val out = PeachPayloadCodec.buildPayload(input, "pw-123")106 val back = PeachPayloadCodec.parsePayload(out, "pw-123")107 assertEquals(3, back.size)108109 val l = back[0]110 assertEquals(login().id, l.id)111 assertEquals("hunter2-Σ-🔐", l.password)112 assertEquals(listOf("dev", "work"), l.tags)113 assertEquals(3, l.breach)114 assertEquals(1_600_000_000_000L, l.passwordChangedAt)115 assertEquals("tripwire", l.canary?.label)116 assertEquals(listOf("https://gist.github.com"), l.additionalUrls)117 assertEquals(1_500_000_000_000L, l.createdAt)118 assertEquals(1_600_000_500_000L, l.modifiedAt)119120 val c = back[2]121 assertEquals("4111111111111111", c.cardNumber)122 assertEquals("visa", c.cardNetwork)123 assertEquals("123", c.cvc)124 }125126 @Test fun v4_round_trips_256_character_ascii_and_multibyte_passwords() {127 for (password in listOf("a".repeat(256), "界".repeat(256))) {128 val encoded = PeachPayloadCodec.buildPayload(listOf(login().copy(password = password)), "pw-123")129 val decoded = PeachPayloadCodec.parsePayload(encoded, "pw-123").single()130131 assertEquals(password, decoded.password)132 }133 }134135 136 137 138 @Test fun export_envelope_has_browser_shape() {139 val out = PeachPayloadCodec.buildPayload(listOf(login()), "pw")140 141 142 assertTrue(out.size >= 16 + 12 + 1024 + 16)143 }144145 @Test fun empty_vault_round_trips() {146 val out = PeachPayloadCodec.buildPayload(emptyList(), "pw")147 assertEquals(0, PeachPayloadCodec.parsePayload(out, "pw").size)148 }149150 @Test fun unicode_fields_round_trip() {151 val out = PeachPayloadCodec.buildPayload(listOf(note()), "pw")152 val n = PeachPayloadCodec.parsePayload(out, "pw").single()153 assertEquals("そふと メモ 🌸", n.title)154 assertEquals("日本語のノート\nsecond line", n.notes)155 }156157 @Test fun wrong_password_is_invalid_passphrase() {158 val out = PeachPayloadCodec.buildPayload(listOf(login()), "right-pw")159 val ex = assertThrows(PeachPayloadCodec.PeachScriptException::class.java) {160 PeachPayloadCodec.parsePayload(out, "wrong-pw")161 }162 assertEquals("INVALID_PASSPHRASE", ex.message)163 }164165 @Test fun pua_codec_round_trips() {166 val bytes = ByteArray(256) { it.toByte() }167 val text = PeachScript.encode(bytes)168 assertTrue(PeachScript.isPeachScript(text))169 assertEquals(256, text.codePointCount(0, text.length))170 assertTrue(bytes.contentEquals(PeachScript.decode(text)))171 172 assertTrue(bytes.contentEquals(PeachScript.decode(PeachScript.normalise(" $text \n"))))173 }174175 @Test fun passkey_core_fields_round_trip() {176 val privateKeyJwk = """{"kty":"EC","crv":"P-256","x":"x1","y":"y1","d":"d1"}"""177 val pk = VaultEntry(178 id = "12121212-3434-5656-7878-909090909090", type = "passkey", title = "Passkey",179 passkey = PasskeyData(180 credentialId = "cred-abc", publicKeyCose = "cose-xyz", rpId = "example.com",181 privateKeyJwk = privateKeyJwk, rpName = "Example",182 userHandle = "uh-1", userName = "june", userDisplayName = "June",183 ),184 createdAt = 1_500_000_000_000, modifiedAt = 1_500_000_000_000,185 )186 val out = PeachPayloadCodec.buildPayload(listOf(pk), "pw")187 val back = PeachPayloadCodec.parsePayload(out, "pw").single()188 assertEquals("passkey", back.type)189 assertNotNull(back.passkey)190 assertEquals("cred-abc", back.passkey?.credentialId)191 assertEquals("example.com", back.passkey?.rpId)192 assertEquals("june", back.passkey?.userName)193 val backJwk = Json.parseToJsonElement(requireNotNull(back.passkey?.privateKeyJwk)).jsonObject194 assertEquals("EC", backJwk["kty"]?.jsonPrimitive?.contentOrNull)195 assertEquals("d1", backJwk["d"]?.jsonPrimitive?.contentOrNull)196 }197198 @Test fun full_export_import_via_peachscript_text() {199 val text = com.peachpasswords.ui.vault.PeachScriptExport.buildEncrypted(listOf(login(), card()), "bp")200 assertTrue(com.peachpasswords.ui.vault.PeachScriptImport.looksLikePeachScript(text))201 val r = com.peachpasswords.ui.vault.PeachScriptImport.decryptAndParse(text, "bp")202 assertTrue(r is com.peachpasswords.ui.vault.PeachScriptImport.DecryptResult.Success)203 r as com.peachpasswords.ui.vault.PeachScriptImport.DecryptResult.Success204 assertEquals(2, r.entries.size)205 assertEquals("hunter2-Σ-🔐", r.entries[0].password)206 assertNull(207 (com.peachpasswords.ui.vault.PeachScriptImport.decryptAndParse(text, "nope")208 as? com.peachpasswords.ui.vault.PeachScriptImport.DecryptResult.Success)?.entries,209 )210 }211212 companion object {213 214 private const val BROWSER_FIXTURE_B64 =215 "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"216 }217}