1package com.peachpasswords.vault23import com.peachpasswords.ui.vault.CsvImport4import org.junit.Assert.assertEquals5import org.junit.Assert.assertNull6import org.junit.Assert.assertTrue7import org.junit.Test8import java.util.Base649101112131415class CxfImportTest {1617 1819 private fun len(len: Int): ByteArray = when {20 len < 0x80 -> byteArrayOf(len.toByte())21 len < 0x100 -> byteArrayOf(0x81.toByte(), len.toByte())22 else -> byteArrayOf(0x82.toByte(), (len shr 8).toByte(), (len and 0xFF).toByte())23 }2425 private fun tlv(tag: Int, content: ByteArray): ByteArray =26 byteArrayOf(tag.toByte()) + len(content.size) + content2728 private fun b64url(bytes: ByteArray): String =29 Base64.getUrlEncoder().withoutPadding().encodeToString(bytes)3031 private fun ed25519Pkcs8(seed: ByteArray): ByteArray {32 val algId = tlv(0x30, tlv(0x06, byteArrayOf(0x2B, 0x65, 0x70)))33 val privKey = tlv(0x04, tlv(0x04, seed))34 return tlv(0x30, tlv(0x02, byteArrayOf(0x00)) + algId + privKey)35 }3637 private fun ecP256Pkcs8(d: ByteArray, pub: ByteArray): ByteArray {38 val ecPubOid = byteArrayOf(0x2A, 0x86.toByte(), 0x48, 0xCE.toByte(), 0x3D, 0x02, 0x01)39 val p256Oid = byteArrayOf(0x2A, 0x86.toByte(), 0x48, 0xCE.toByte(), 0x3D, 0x03, 0x01, 0x07)40 val algId = tlv(0x30, tlv(0x06, ecPubOid) + tlv(0x06, p256Oid))41 val ecPriv = tlv(42 0x30,43 tlv(0x02, byteArrayOf(0x01)) +44 tlv(0x04, d) +45 tlv(0xA1, tlv(0x03, byteArrayOf(0x00) + pub)),46 )47 return tlv(0x30, tlv(0x02, byteArrayOf(0x00)) + algId + tlv(0x04, ecPriv))48 }4950 private val seed = ByteArray(32) { it.toByte() }51 private val dScalar = ByteArray(32) { (0x40 + it).toByte() }52 private val pub = ByteArray(65) { 0x04 }5354 private fun cxfJson(keyB64url: String, type: String = "passkey") = """55 {56 "accounts": [57 {58 "userId": "acct-1",59 "items": [60 {61 "title": "example.com",62 "credentials": [63 {64 "type": "$type",65 "credentialId": "Y3JlZC1pZA",66 "rpId": "example.com",67 "username": "[email protected]",68 "userDisplayName": "June",69 "userHandle": "dXNlci1oYW5kbGU",70 "key": "$keyB64url"71 }72 ]73 }74 ]75 }76 ]77 }78 """.trimIndent()7980 @Test fun detects_cxf_json() {81 assertEquals(CsvImport.CXF_JSON, CsvImport.detectProvider(cxfJson(b64url(ed25519Pkcs8(seed)))))82 }8384 @Test fun cxf_wins_over_bitwarden_items_sniff() {85 86 val json = cxfJson(b64url(ed25519Pkcs8(seed)))87 assertTrue(json.contains("\"items\""))88 assertEquals(CsvImport.CXF_JSON, CsvImport.detectProvider(json))89 }9091 @Test fun parses_ed25519_passkey() {92 val entries = CsvImport.parse(cxfJson(b64url(ed25519Pkcs8(seed))), CsvImport.CXF_JSON)93 assertEquals(1, entries.size)94 val entry = entries[0]95 assertEquals("example.com", entry.title)96 assertEquals("https://example.com", entry.url)97 98 assertNull(entry.username)99 assertNull(entry.password)100 assertEquals(1, entry.passkeys.size)101102 val pk = entry.passkeys[0]103 assertEquals("Y3JlZC1pZA", pk.credentialId)104 assertEquals("example.com", pk.rpId)105 assertEquals("example.com", pk.rpName)106 assertEquals(-8, pk.algorithm)107 assertEquals("dXNlci1oYW5kbGU", pk.userHandle)108 assertEquals("[email protected]", pk.userName)109 assertEquals("June", pk.userDisplayName)110 assertTrue(pk.privateKeyJwk.contains("\"kty\":\"OKP\""))111 assertTrue(pk.privateKeyJwk.contains("\"crv\":\"Ed25519\""))112 assertTrue(pk.privateKeyJwk.contains("\"d\":\"" + b64url(seed) + "\""))113 }114115 @Test fun parses_ec_p256_passkey() {116 val entries = CsvImport.parse(cxfJson(b64url(ecP256Pkcs8(dScalar, pub))), CsvImport.CXF_JSON)117 val pk = entries[0].passkeys[0]118 assertEquals(-7, pk.algorithm)119 assertTrue(pk.privateKeyJwk.contains("\"kty\":\"EC\""))120 assertTrue(pk.privateKeyJwk.contains("\"crv\":\"P-256\""))121 assertTrue(pk.privateKeyJwk.contains("\"d\":\"" + b64url(dScalar) + "\""))122 }123124 @Test fun non_passkey_credential_is_skipped() {125 val entries = CsvImport.parse(cxfJson(b64url(ed25519Pkcs8(seed)), type = "password"), CsvImport.CXF_JSON)126 assertTrue(entries.isEmpty())127 }128129 @Test fun unreadable_key_is_skipped() {130 val entries = CsvImport.parse(cxfJson("!!!not-base64url!!!"), CsvImport.CXF_JSON)131 assertTrue(entries.isEmpty())132 }133134 @Test fun missing_required_field_is_skipped() {135 136 val json = """137 {138 "accounts": [139 { "items": [ { "title": "x.com", "credentials": [140 { "type": "passkey", "credentialId": "Y3JlZA", "rpId": "x.com",141 "username": "u", "userDisplayName": "U",142 "key": "${b64url(ed25519Pkcs8(seed))}" }143 ] } ] }144 ]145 }146 """.trimIndent()147 assertTrue(CsvImport.parse(json, CsvImport.CXF_JSON).isEmpty())148 }149}