package com.peachpasswords.vault import com.peachpasswords.ui.vault.CsvImport import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import java.util.Base64 /** * CXF (FIDO Alliance Credential Exchange Format) passkey import. The private * keys are real PKCS#8 DER, assembled here so the parser's OID detection and * `d` extraction are genuinely exercised rather than stubbed. */ class CxfImportTest { // ---- tiny DER builder so the vectors are self-assembling --------------- private fun len(len: Int): ByteArray = when { len < 0x80 -> byteArrayOf(len.toByte()) len < 0x100 -> byteArrayOf(0x81.toByte(), len.toByte()) else -> byteArrayOf(0x82.toByte(), (len shr 8).toByte(), (len and 0xFF).toByte()) } private fun tlv(tag: Int, content: ByteArray): ByteArray = byteArrayOf(tag.toByte()) + len(content.size) + content private fun b64url(bytes: ByteArray): String = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes) private fun ed25519Pkcs8(seed: ByteArray): ByteArray { val algId = tlv(0x30, tlv(0x06, byteArrayOf(0x2B, 0x65, 0x70))) val privKey = tlv(0x04, tlv(0x04, seed)) return tlv(0x30, tlv(0x02, byteArrayOf(0x00)) + algId + privKey) } private fun ecP256Pkcs8(d: ByteArray, pub: ByteArray): ByteArray { val ecPubOid = byteArrayOf(0x2A, 0x86.toByte(), 0x48, 0xCE.toByte(), 0x3D, 0x02, 0x01) val p256Oid = byteArrayOf(0x2A, 0x86.toByte(), 0x48, 0xCE.toByte(), 0x3D, 0x03, 0x01, 0x07) val algId = tlv(0x30, tlv(0x06, ecPubOid) + tlv(0x06, p256Oid)) val ecPriv = tlv( 0x30, tlv(0x02, byteArrayOf(0x01)) + tlv(0x04, d) + tlv(0xA1, tlv(0x03, byteArrayOf(0x00) + pub)), ) return tlv(0x30, tlv(0x02, byteArrayOf(0x00)) + algId + tlv(0x04, ecPriv)) } private val seed = ByteArray(32) { it.toByte() } private val dScalar = ByteArray(32) { (0x40 + it).toByte() } private val pub = ByteArray(65) { 0x04 } private fun cxfJson(keyB64url: String, type: String = "passkey") = """ { "accounts": [ { "userId": "acct-1", "items": [ { "title": "example.com", "credentials": [ { "type": "$type", "credentialId": "Y3JlZC1pZA", "rpId": "example.com", "username": "june@example.com", "userDisplayName": "June", "userHandle": "dXNlci1oYW5kbGU", "key": "$keyB64url" } ] } ] } ] } """.trimIndent() @Test fun detects_cxf_json() { assertEquals(CsvImport.CXF_JSON, CsvImport.detectProvider(cxfJson(b64url(ed25519Pkcs8(seed))))) } @Test fun cxf_wins_over_bitwarden_items_sniff() { // CXF nests "items" under each account; the accounts sniff must win. val json = cxfJson(b64url(ed25519Pkcs8(seed))) assertTrue(json.contains("\"items\"")) assertEquals(CsvImport.CXF_JSON, CsvImport.detectProvider(json)) } @Test fun parses_ed25519_passkey() { val entries = CsvImport.parse(cxfJson(b64url(ed25519Pkcs8(seed))), CsvImport.CXF_JSON) assertEquals(1, entries.size) val entry = entries[0] assertEquals("example.com", entry.title) assertEquals("https://example.com", entry.url) // Passkey-only: no login fields populated, so no empty login is spawned. assertNull(entry.username) assertNull(entry.password) assertEquals(1, entry.passkeys.size) val pk = entry.passkeys[0] assertEquals("Y3JlZC1pZA", pk.credentialId) assertEquals("example.com", pk.rpId) assertEquals("example.com", pk.rpName) assertEquals(-8, pk.algorithm) assertEquals("dXNlci1oYW5kbGU", pk.userHandle) assertEquals("june@example.com", pk.userName) assertEquals("June", pk.userDisplayName) assertTrue(pk.privateKeyJwk.contains("\"kty\":\"OKP\"")) assertTrue(pk.privateKeyJwk.contains("\"crv\":\"Ed25519\"")) assertTrue(pk.privateKeyJwk.contains("\"d\":\"" + b64url(seed) + "\"")) } @Test fun parses_ec_p256_passkey() { val entries = CsvImport.parse(cxfJson(b64url(ecP256Pkcs8(dScalar, pub))), CsvImport.CXF_JSON) val pk = entries[0].passkeys[0] assertEquals(-7, pk.algorithm) assertTrue(pk.privateKeyJwk.contains("\"kty\":\"EC\"")) assertTrue(pk.privateKeyJwk.contains("\"crv\":\"P-256\"")) assertTrue(pk.privateKeyJwk.contains("\"d\":\"" + b64url(dScalar) + "\"")) } @Test fun non_passkey_credential_is_skipped() { val entries = CsvImport.parse(cxfJson(b64url(ed25519Pkcs8(seed)), type = "password"), CsvImport.CXF_JSON) assertTrue(entries.isEmpty()) } @Test fun unreadable_key_is_skipped() { val entries = CsvImport.parse(cxfJson("!!!not-base64url!!!"), CsvImport.CXF_JSON) assertTrue(entries.isEmpty()) } @Test fun missing_required_field_is_skipped() { // Drop userHandle — the browser requires all six fields. val json = """ { "accounts": [ { "items": [ { "title": "x.com", "credentials": [ { "type": "passkey", "credentialId": "Y3JlZA", "rpId": "x.com", "username": "u", "userDisplayName": "U", "key": "${b64url(ed25519Pkcs8(seed))}" } ] } ] } ] } """.trimIndent() assertTrue(CsvImport.parse(json, CsvImport.CXF_JSON).isEmpty()) } }