1package com.peachpasswords.passkeys23import com.peachpasswords.vault.PasskeyData4import com.peachpasswords.vault.VaultEntry5import org.junit.Assert.assertEquals6import org.junit.Assert.assertTrue7import org.junit.Test89class PasskeyCredentialPlannerTest {10 private val webCaller = "https://example.com"1112 @Test fun matches_passkeys_by_exact_rp_id() {13 val matches = PasskeyCredentialPlanner.candidates(14 listOf(15 passkey("a", rpId = "example.com", username = "june"),16 passkey("b", rpId = "evil-example.com", username = "lookalike"),17 ),18 """{"challenge":"c","rpId":"example.com"}""",19 callerOrigin = webCaller,20 )2122 assertEquals(listOf("a"), matches.map { it.entryId })23 assertEquals("june", matches.single().username)24 }2526 @Test fun narrows_to_allow_credentials_when_present() {27 val matches = PasskeyCredentialPlanner.candidates(28 listOf(29 passkey("a", credentialId = "cred-a", lastUsedAt = 10),30 passkey("b", credentialId = "cred-b", lastUsedAt = 20),31 ),32 """{"challenge":"c","rpId":"example.com","allowCredentials":[{"type":"public-key","id":"cred-a"}]}""",33 callerOrigin = webCaller,34 )3536 assertEquals(listOf("a"), matches.map { it.entryId })37 }3839 @Test fun empty_or_invalid_requests_offer_no_credentials() {40 assertTrue(PasskeyCredentialPlanner.candidates(listOf(passkey("a")), """{"challenge":"c"}""", webCaller).isEmpty())41 assertTrue(PasskeyCredentialPlanner.candidates(listOf(passkey("a")), "not-json", webCaller).isEmpty())42 }4344 @Test fun unknown_caller_gets_no_credentials() {45 assertTrue(PasskeyCredentialPlanner.candidates(listOf(passkey("a")), """{"challenge":"c","rpId":"example.com"}""", null).isEmpty())46 assertTrue(PasskeyCredentialPlanner.candidates(listOf(passkey("a")), """{"challenge":"c","rpId":"example.com"}""", " ").isEmpty())47 }4849 @Test fun plaintext_http_callers_get_no_credentials() {50 assertTrue(51 PasskeyCredentialPlanner.candidates(52 listOf(passkey("a")),53 """{"challenge":"c","rpId":"example.com"}""",54 "http://example.com",55 ).isEmpty(),56 )57 }5859 @Test fun legacy_credentials_hide_from_native_app_callers() {60 61 62 63 val nativeCaller = "android:apk-key-hash:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"64 assertTrue(65 PasskeyCredentialPlanner.candidates(66 listOf(passkey("a")),67 """{"challenge":"c","rpId":"example.com"}""",68 nativeCaller,69 ).isEmpty(),70 )71 }7273 @Test fun bound_credentials_only_go_to_their_bound_caller() {74 val boundOrigin = "android:apk-key-hash:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"75 val otherApp = "android:apk-key-hash:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"76 val entries = listOf(passkey("a", callerOrigin = boundOrigin))77 val request = """{"challenge":"c","rpId":"example.com"}"""7879 assertEquals(listOf("a"), PasskeyCredentialPlanner.candidates(entries, request, boundOrigin).map { it.entryId })80 assertTrue(PasskeyCredentialPlanner.candidates(entries, request, otherApp).isEmpty())81 82 assertTrue(PasskeyCredentialPlanner.candidates(entries, request, webCaller).isEmpty())83 }8485 private fun passkey(86 id: String,87 rpId: String = "example.com",88 credentialId: String = "cred-$id",89 username: String = "user-$id",90 lastUsedAt: Long? = null,91 callerOrigin: String? = null,92 ) = VaultEntry(93 id = id,94 type = "passkey",95 title = "Passkey $id",96 passkey = PasskeyData(97 credentialId = credentialId,98 publicKeyCose = "cose-$id",99 rpId = rpId,100 userHandle = "handle-$id",101 userName = username,102 userDisplayName = "User $id",103 callerOrigin = callerOrigin,104 ),105 lastUsedAt = lastUsedAt,106 createdAt = 1,107 modifiedAt = 1,108 )109}