d993f918cad9

fix: Unify Samsung passkey paths and open chooser (v2.16.1)

- Use the authenticated BeginGet client-data hash for both row and confirmation actions - Carry chooser inputs directly when Samsung omits the Action request attachment - Preserve RP, caller-origin, and allowCredentials filtering - Verify 1,094 unit tests, lint, and signed release assembly

time
2026-09-06 18:00 UTC · 1 month ago
machine
koh2
parent
e3315622e288
3 files VERSION+1 −1app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt+40 −10app/src/test/java/com/peachpasswords/passkeys/CredentialProviderContractTest.kt+2 −2
VERSION+1 −1
@@ -1 +1 @@1-2.16.01+2.16.1

view file

app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt+40 −10
@@ -131,13 +131,36 @@131131         val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)132132         val origin = request?.callingAppInfo?.let { PasskeyCallerOrigin.resolve(it, allowlist) }133133             ?: intent.getStringExtra(EXTRA_AUTHENTICATED_ORIGIN)134-        val choices = request?.beginGetCredentialOptions134+        val requestChoices = request?.beginGetCredentialOptions135135             ?.filterIsInstance<BeginGetPublicKeyCredentialOption>()136136             ?.flatMap { option ->137137                 PasskeyCredentialPlanner.candidates(vault.getEntries(), option.requestJson, origin)138-                    .map { ManualChoice(it.entryId, it.displayName, it.username, option) }138+                    .map {139+                        ManualChoice(140+                            it.entryId,141+                            it.displayName,142+                            it.username,143+                            option.requestJson,144+                            option.clientDataHash,145+                        )146+                    }139147             }140148             .orEmpty()149+        val choices = requestChoices.ifEmpty {150+            val requestJson = intent.getStringExtra(EXTRA_REQUEST_JSON)151+            val authenticatedHash = intent.getByteArrayExtra(EXTRA_AUTHENTICATED_CLIENT_DATA_HASH)152+            if (requestJson == null) emptyList()153+            else PasskeyCredentialPlanner.candidates(vault.getEntries(), requestJson, origin)154+                .map {155+                    ManualChoice(156+                        entryId = it.entryId,157+                        displayName = it.displayName,158+                        username = it.username,159+                        requestJson = requestJson,160+                        clientDataHash = authenticatedHash,161+                    )162+                }163+        }141164         if (choices.isEmpty()) {142165             finish()143166             return@@ -179,15 +202,15 @@179202                         },180203                     )181204                     LazyColumn {182-                        items(visible, key = { "${it.entryId}:${it.option.requestJson.hashCode()}" }) { choice ->205+                        items(visible, key = { "${it.entryId}:${it.requestJson.hashCode()}" }) { choice ->183206                             Column(184207                                 Modifier.fillMaxWidth().clickable {185208                                     lifecycleScope.launch {186209                                         completeAssertion(187210                                             choice.entryId,188-                                            choice.option.requestJson,211+                                            choice.requestJson,189212                                             origin,190-                                            choice.option.clientDataHash,213+                                            choice.clientDataHash,191214                                         )192215                                     }193216                                 }.padding(vertical = 12.dp),@@ -447,10 +470,15 @@447470             request: BeginGetCredentialRequest,448471             authenticatedOrigin: String?,449472         ): PendingIntent {473+            val option = request.beginGetCredentialOptions474+                .filterIsInstance<BeginGetPublicKeyCredentialOption>()475+                .firstOrNull()450476             val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)451477                 .putExtra(EXTRA_MODE, MODE_CHOOSE)452478                 .putExtra(EXTRA_BEGIN_REQUEST, BeginGetCredentialRequest.asBundle(request))453479                 .putExtra(EXTRA_AUTHENTICATED_ORIGIN, authenticatedOrigin)480+                .putExtra(EXTRA_REQUEST_JSON, option?.requestJson)481+                .putExtra(EXTRA_AUTHENTICATED_CLIENT_DATA_HASH, option?.clientDataHash)454482             val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0455483             return PendingIntent.getActivity(456484                 context,@@ -465,7 +493,8 @@465493         val entryId: String,466494         val displayName: String,467495         val username: String,468-        val option: BeginGetPublicKeyCredentialOption,496+        val requestJson: String,497+        val clientDataHash: ByteArray?,469498     )470499 }471500 @@ -481,11 +510,12 @@481510         ?: authenticatedOrigin?.takeIf { it.isNotBlank() }482511 483512 /**484- * Prefer request data attached by Credential Manager to the selected row. Some485- * OEM confirmation-button paths omit that final request, so retain the same486- * framework-authenticated hash that arrived in the BeginGet request.513+ * Keep the framework-authenticated BeginGet hash whenever it is available.514+ * Samsung can omit or replace the final row request depending on whether the515+ * row or confirmation button was tapped; both UI paths must sign identical516+ * client data.487517  */488518 internal fun selectionClientDataHash(489519     frameworkHash: ByteArray?,490520     authenticatedHash: ByteArray?,491-): ByteArray? = frameworkHash ?: authenticatedHash521+): ByteArray? = authenticatedHash ?: frameworkHash

view file

app/src/test/java/com/peachpasswords/passkeys/CredentialProviderContractTest.kt+2 −2
@@ -43,13 +43,13 @@4343     }4444 4545     @Test46-    fun selectionPrefersTheFinalFrameworkHash() {46+    fun selectionKeepsTheAuthenticatedBeginHashWhenSamsungReplacesFinalHash() {4747         val frameworkHash = byteArrayOf(4, 5, 6)4848         val authenticatedHash = byteArrayOf(1, 2, 3)4949 5050         assertTrue(5151             selectionClientDataHash(frameworkHash, authenticatedHash)!!52-                .contentEquals(frameworkHash),52+                .contentEquals(authenticatedHash),5353         )5454     }5555 

view file