d79cd9e7a5f0
fix: Complete locked passkey selection (v2.15.8)
- time
- 2026-09-06 14:33 UTC · 1 month ago
- machine
- koh2
- parent
- 647ed7040b49
↓ peach-android-d79cd9e7.tar.gzatlas at this save
files changed6 files
VERSION+1 −1app/src/main/java/com/peachpasswords/credentials/CredentialProviderUnlockActivity.kt+1 −0app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt+25 −2app/src/main/java/com/peachpasswords/passkeys/PasskeyUnlockIntent.kt+5 −1app/src/main/java/com/peachpasswords/passkeys/PeachCredentialProviderService.kt+1 −0app/src/test/java/com/peachpasswords/passkeys/CredentialProviderContractTest.kt+40 −0app/src/main/java/com/peachpasswords/credentials/CredentialProviderUnlockActivity.kt+1 −0
@@ -113,6 +113,7 @@113113 this,114114 candidate.entryId,115115 option.requestJson,116+ callerOrigin,116117 ),117118 beginGetPublicKeyCredentialOption = option,118119 displayName = candidate.displayName,app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt+25 −2
@@ -83,9 +83,13 @@8383 // F1/F2: the origin must be framework-verified (privileged browser8484 // allowlist, else the caller's apk-key-hash). Unresolvable callers8585 // fail closed — PasskeyAssertion.build refuses a null origin.86- val origin = providerRequest?.callingAppInfo?.let { info ->86+ val frameworkOrigin = providerRequest?.callingAppInfo?.let { info ->8787 PasskeyCallerOrigin.resolve(info, PasskeyCallerOrigin.privilegedAllowlist(this))8888 }89+ val origin = selectionOrigin(90+ frameworkOrigin = frameworkOrigin,91+ authenticatedOrigin = intent.getStringExtra(EXTRA_AUTHENTICATED_ORIGIN),92+ )8993 // H-07: WebAuthn's UV flag must reflect actual user verification, not a9094 // hard-coded bit. If the relying party demands UV, run the platform9195 // biometric / device-credential prompt and only sign with UV=1 on@@ -271,6 +275,8 @@271275 companion object {272276 private const val EXTRA_ENTRY_ID = "com.peachpasswords.passkeys.EXTRA_ENTRY_ID"273277 private const val EXTRA_REQUEST_JSON = "com.peachpasswords.passkeys.EXTRA_REQUEST_JSON"278+ private const val EXTRA_AUTHENTICATED_ORIGIN =279+ "com.peachpasswords.passkeys.EXTRA_AUTHENTICATED_ORIGIN"274280 private const val EXTRA_MODE = "com.peachpasswords.passkeys.EXTRA_MODE"275281 private const val MODE_CREATE = "create"276282 // WebAuthn's PublicKeyCredentialRequestOptions.userVerification values.@@ -278,10 +284,16 @@278284 // "preferred" (default) and "discouraged" leave UV=0 in authenticator data.279285 private const val UV_REQUIRED = "required"280286 281- fun pendingIntent(context: Context, entryId: String, requestJson: String): PendingIntent {287+ fun pendingIntent(288+ context: Context,289+ entryId: String,290+ requestJson: String,291+ authenticatedOrigin: String?,292+ ): PendingIntent {282293 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)283294 .putExtra(EXTRA_ENTRY_ID, entryId)284295 .putExtra(EXTRA_REQUEST_JSON, requestJson)296+ .putExtra(EXTRA_AUTHENTICATED_ORIGIN, authenticatedOrigin)285297 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0286298 return PendingIntent.getActivity(287299 context,@@ -305,3 +317,14 @@305317 }306318 }307319 }320+321+/**322+ * The authentication-action response is built only after Peach has verified323+ * the caller from the framework's BeginGetCredentialRequest. Some OEM324+ * Credential Manager implementations omit the final provider request when a325+ * credential row is returned from that nested response, so retain that same326+ * verified origin as a fallback instead of turning a valid row into a no-op.327+ */328+internal fun selectionOrigin(frameworkOrigin: String?, authenticatedOrigin: String?): String? =329+ frameworkOrigin?.takeIf { it.isNotBlank() }330+ ?: authenticatedOrigin?.takeIf { it.isNotBlank() }app/src/main/java/com/peachpasswords/passkeys/PasskeyUnlockIntent.kt+5 −1
@@ -3,6 +3,7 @@33 import android.app.PendingIntent44 import android.content.Context55 import android.content.Intent6+import android.os.Build67 import androidx.credentials.provider.BeginGetCredentialRequest78 import com.peachpasswords.credentials.CredentialProviderUnlockActivity89 @@ -14,9 +15,12 @@1415 context,1516 REQUEST_CODE,1617 intent,17- PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,18+ PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag(),1819 )1920 }21+22+ private fun mutableFlag(): Int =23+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 02024 2125 const val EXTRA_REQUEST = "com.peachpasswords.passkeys.EXTRA_BEGIN_GET_REQUEST"2226 private const val REQUEST_CODE = 0x504bapp/src/main/java/com/peachpasswords/passkeys/PeachCredentialProviderService.kt+1 −0
@@ -108,6 +108,7 @@108108 this,109109 candidate.entryId,110110 option.requestJson,111+ callerOrigin,111112 ),112113 beginGetPublicKeyCredentialOption = option,113114 displayName = candidate.displayName,app/src/test/java/com/peachpasswords/passkeys/CredentialProviderContractTest.kt+40 −0
@@ -0,0 +1,40 @@1+package com.peachpasswords.passkeys2+3+import java.io.File4+import org.junit.Assert.assertFalse5+import org.junit.Assert.assertTrue6+import org.junit.Test7+8+class CredentialProviderContractTest {9+ @Test10+ fun authenticationActionPendingIntentIsMutableForFrameworkRequestAttachment() {11+ val source = source("passkeys/PasskeyUnlockIntent.kt")12+13+ assertTrue(source.contains("PendingIntent.FLAG_MUTABLE"))14+ assertFalse(source.contains("PendingIntent.FLAG_IMMUTABLE"))15+ }16+17+ @Test18+ fun selectionPrefersTheFinalFrameworkOrigin() {19+ assertTrue(20+ selectionOrigin("https://github.com", "https://example.com") ==21+ "https://github.com",22+ )23+ }24+25+ @Test26+ fun selectionRetainsAuthenticatedBeginRequestOriginWhenOemOmitsFinalRequest() {27+ assertTrue(28+ selectionOrigin(null, "https://github.com") == "https://github.com",29+ )30+ }31+32+ @Test33+ fun selectionStillFailsClosedWithoutAnyVerifiedOrigin() {34+ assertTrue(selectionOrigin(null, null) == null)35+ assertTrue(selectionOrigin(" ", "") == null)36+ }37+38+ private fun source(relativePath: String): String =39+ File("src/main/java/com/peachpasswords/$relativePath").readText()40+}