484701884efc

fix: Separate passkey payload from UV policy (v2.16.3)

- Restore Samsung's final request JSON for assertion construction - Preserve the authenticated BeginGet JSON as a non-downgradable biometric requirement - Keep final-request fallback for incomplete OEM callbacks - Verify 1,097 unit tests, lint, and signed release assembly

time
2026-09-06 19:40 UTC · 1 month ago
machine
koh2
parent
832644cd88ad
3 files VERSION+1 −1app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt+27 −5app/src/test/java/com/peachpasswords/passkeys/CredentialProviderContractTest.kt+12 −2
VERSION+1 −1
@@ -1 +1 @@1-2.16.21+2.16.3

view file

app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt+27 −5
@@ -105,9 +105,10 @@105105         val option = providerRequest?.credentialOptions106106             ?.filterIsInstance<GetPublicKeyCredentialOption>()107107             ?.firstOrNull()108+        val authenticatedRequestJson = intent.getStringExtra(EXTRA_REQUEST_JSON)108109         val requestJson = selectionRequestJson(109110             frameworkJson = option?.requestJson,110-            authenticatedJson = intent.getStringExtra(EXTRA_REQUEST_JSON),111+            authenticatedJson = authenticatedRequestJson,111112         )112113         val clientDataHash = selectionClientDataHash(113114             frameworkHash = option?.clientDataHash,@@ -123,7 +124,16 @@123124             frameworkOrigin = frameworkOrigin,124125             authenticatedOrigin = intent.getStringExtra(EXTRA_AUTHENTICATED_ORIGIN),125126         )126-        completeAssertion(entryId, requestJson, origin, clientDataHash)127+        completeAssertion(128+            entryId,129+            requestJson,130+            origin,131+            clientDataHash,132+            uvRequiredOverride = selectionRequiresUserVerification(133+                frameworkJson = option?.requestJson,134+                authenticatedJson = authenticatedRequestJson,135+            ),136+        )127137     }128138 129139     private fun showManualChooser() {@@ -233,6 +243,7 @@233243         requestJson: String?,234244         origin: String?,235245         clientDataHash: ByteArray?,246+        uvRequiredOverride: Boolean? = null,236247     ) {237248         val result = Intent()238249         val entry = entryId?.let(vault::getEntry)@@ -242,7 +253,7 @@242253         // success. If UV is required but verification fails, do NOT sign —243254         // fail closed rather than produce a misleading UV=0 assertion.244255         val uvRequired = entry != null && requestJson != null &&245-            userVerificationRequirement(requestJson) == UV_REQUIRED256+            (uvRequiredOverride ?: (userVerificationRequirement(requestJson) == UV_REQUIRED))246257         val userVerified = if (uvRequired) {247258             runUserVerificationPrompt()248259         } else {@@ -523,8 +534,19 @@523534     authenticatedHash: ByteArray?,524535 ): ByteArray? = authenticatedHash ?: frameworkHash525536 526-/** Keep UV/challenge semantics from the framework-authenticated BeginGet request. */537+/** Use the final assertion JSON when present; its challenge pairs with the final request. */527538 internal fun selectionRequestJson(528539     frameworkJson: String?,529540     authenticatedJson: String?,530-): String? = authenticatedJson ?: frameworkJson541+): String? = frameworkJson ?: authenticatedJson542+543+/** Never let an OEM's final row payload weaken BeginGet's UV requirement. */544+internal fun selectionRequiresUserVerification(545+    frameworkJson: String?,546+    authenticatedJson: String?,547+): Boolean = listOfNotNull(frameworkJson, authenticatedJson).any { requestJson ->548+    runCatching {549+        Json.parseToJsonElement(requestJson).jsonObject["userVerification"]550+            ?.jsonPrimitive?.contentOrNull == "required"551+    }.getOrDefault(false)552+}

view file

app/src/test/java/com/peachpasswords/passkeys/CredentialProviderContractTest.kt+12 −2
@@ -54,13 +54,13 @@5454     }5555 5656     @Test57-    fun selectionKeepsAuthenticatedBeginJsonWhenSamsungDropsUvRequirement() {57+    fun selectionKeepsFinalJsonForTheSignedAssertion() {5858         val frameworkJson = """{"challenge":"final"}"""5959         val authenticatedJson =6060             """{"challenge":"begin","userVerification":"required"}"""6161 6262         assertTrue(63-            selectionRequestJson(frameworkJson, authenticatedJson) == authenticatedJson,63+            selectionRequestJson(frameworkJson, authenticatedJson) == frameworkJson,6464         )6565     }6666 @@ -69,6 +69,16 @@6969         val frameworkJson = """{"challenge":"final"}"""7070 7171         assertTrue(selectionRequestJson(frameworkJson, null) == frameworkJson)72+    }73+74+    @Test75+    fun authenticatedBeginJsonCanRequireUvWithoutReplacingFinalAssertionJson() {76+        val frameworkJson = """{"challenge":"final"}"""77+        val authenticatedJson =78+            """{"challenge":"begin","userVerification":"required"}"""79+80+        assertTrue(selectionRequiresUserVerification(frameworkJson, authenticatedJson))81+        assertTrue(selectionRequestJson(frameworkJson, authenticatedJson) == frameworkJson)7282     }7383 7484     @Test

view file