1package com.peachpasswords.vault23import com.peachpasswords.ui.vault.CsvImport4import org.junit.Assert.assertEquals5import org.junit.Assert.assertTrue6import org.junit.Test78class BitwardenPasskeyImportTest {910 private val bitwardenWithPasskey = """11 {12 "encrypted": false,13 "items": [14 {15 "id": "abc-123",16 "type": 1,17 "name": "example.com",18 "login": {19 "username": "[email protected]",20 "password": "hunter2",21 "totp": null,22 "uris": [{"match": null, "uri": "https://example.com/login"}],23 "fido2Credentials": [24 {25 "credentialId": "Y3JlZC1pZA",26 "keyType": "public-key",27 "keyAlgorithm": "ECDSA",28 "keyCurve": "P-256",29 "keyValue": "cHJpdmF0ZS1rZXktc2NhbGFy",30 "rpId": "example.com",31 "rpName": "Example Inc",32 "userHandle": "dXNlci1oYW5kbGU",33 "userName": "[email protected]",34 "userDisplayName": "June",35 "counter": 7,36 "discoverable": true,37 "creationDate": "2025-06-01T00:00:00Z"38 }39 ]40 },41 "notes": null42 }43 ]44 }45 """.trimIndent()4647 private val bitwardenNoPasskey = """48 {49 "encrypted": false,50 "items": [51 {52 "id": "def-456",53 "type": 1,54 "name": "plain.com",55 "login": {56 "username": "user",57 "password": "pass",58 "uris": [{"match": null, "uri": "https://plain.com"}]59 }60 }61 ]62 }63 """.trimIndent()6465 @Test fun detects_bitwarden_json() {66 assertEquals(CsvImport.BITWARDEN_JSON, CsvImport.detectProvider(bitwardenWithPasskey))67 }6869 @Test fun parses_login_with_passkey() {70 val entries = CsvImport.parse(bitwardenWithPasskey, CsvImport.BITWARDEN_JSON)71 assertEquals(1, entries.size)72 val entry = entries[0]73 assertEquals("example.com", entry.title)74 assertEquals("[email protected]", entry.username)75 assertEquals("hunter2", entry.password)76 assertEquals(1, entry.passkeys.size)7778 val pk = entry.passkeys[0]79 assertEquals("Y3JlZC1pZA", pk.credentialId)80 assertEquals("example.com", pk.rpId)81 assertEquals("Example Inc", pk.rpName)82 assertEquals(-7, pk.algorithm)83 assertEquals("dXNlci1oYW5kbGU", pk.userHandle)84 assertEquals("[email protected]", pk.userName)85 assertEquals("June", pk.userDisplayName)86 assertEquals(7, pk.signCount)87 assertTrue(pk.privateKeyJwk.contains("\"kty\":\"EC\""))88 assertTrue(pk.privateKeyJwk.contains("\"d\":\"cHJpdmF0ZS1rZXktc2NhbGFy\""))89 }9091 @Test fun login_without_passkey_has_empty_passkeys() {92 val entries = CsvImport.parse(bitwardenNoPasskey, CsvImport.BITWARDEN_JSON)93 assertEquals(1, entries.size)94 assertTrue(entries[0].passkeys.isEmpty())95 }9697 @Test fun ed25519_passkey_maps_to_algorithm_minus_8() {98 val json = """99 {100 "items": [101 {102 "type": 1,103 "name": "ed.example.com",104 "login": {105 "username": "u",106 "fido2Credentials": [107 {108 "credentialId": "Y3JlZA",109 "keyAlgorithm": "EdDSA",110 "keyCurve": "Ed25519",111 "keyValue": "c2VlZA",112 "rpId": "ed.example.com",113 "counter": 0114 }115 ]116 }117 }118 ]119 }120 """.trimIndent()121 val entries = CsvImport.parse(json, CsvImport.BITWARDEN_JSON)122 val pk = entries[0].passkeys[0]123 assertEquals(-8, pk.algorithm)124 assertTrue(pk.privateKeyJwk.contains("\"kty\":\"OKP\""))125 }126127 @Test fun unsupported_algorithm_is_skipped() {128 val json = """129 {130 "items": [131 {132 "type": 1,133 "name": "rsa.example.com",134 "login": {135 "username": "u",136 "fido2Credentials": [137 {138 "credentialId": "Y3JlZA",139 "keyAlgorithm": "RSA",140 "keyCurve": "",141 "keyValue": "a2V5",142 "rpId": "rsa.example.com",143 "counter": 0144 }145 ]146 }147 }148 ]149 }150 """.trimIndent()151 val entries = CsvImport.parse(json, CsvImport.BITWARDEN_JSON)152 assertTrue(entries[0].passkeys.isEmpty())153 }154}