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 legacy_credentials_hide_from_native_app_callers() {50 51 52 53 val nativeCaller = "android:apk-key-hash:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"54 assertTrue(55 PasskeyCredentialPlanner.candidates(56 listOf(passkey("a")),57 """{"challenge":"c","rpId":"example.com"}""",58 nativeCaller,59 ).isEmpty(),60 )61 }6263 @Test fun bound_credentials_only_go_to_their_bound_caller() {64 val boundOrigin = "android:apk-key-hash:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"65 val otherApp = "android:apk-key-hash:BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"66 val entries = listOf(passkey("a", callerOrigin = boundOrigin))67 val request = """{"challenge":"c","rpId":"example.com"}"""6869 assertEquals(listOf("a"), PasskeyCredentialPlanner.candidates(entries, request, boundOrigin).map { it.entryId })70 assertTrue(PasskeyCredentialPlanner.candidates(entries, request, otherApp).isEmpty())71 72 assertTrue(PasskeyCredentialPlanner.candidates(entries, request, webCaller).isEmpty())73 }7475 private fun passkey(76 id: String,77 rpId: String = "example.com",78 credentialId: String = "cred-$id",79 username: String = "user-$id",80 lastUsedAt: Long? = null,81 callerOrigin: String? = null,82 ) = VaultEntry(83 id = id,84 type = "passkey",85 title = "Passkey $id",86 passkey = PasskeyData(87 credentialId = credentialId,88 publicKeyCose = "cose-$id",89 rpId = rpId,90 userHandle = "handle-$id",91 userName = username,92 userDisplayName = "User $id",93 callerOrigin = callerOrigin,94 ),95 lastUsedAt = lastUsedAt,96 createdAt = 1,97 modifiedAt = 1,98 )99}