PasskeyCredentialPlannerTest.kt

4.0 KB · raw

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        // callerOrigin == null means pre-binding (or browser-synced). A native51        // app's apk-key-hash origin can never prove a relationship to the52        // rpId, so legacy credentials stay hidden from it.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        // Web callers also cannot take a credential bound to a native app.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}