ff53d8a9b4be
fix: Preserve authenticated credential handoff (v2.17.2)
- time
- 2026-09-08 00:33 UTC · 1 month ago
- machine
- koh2
- parent
- 7f47a411cc3a
↓ peach-android-ff53d8a9.tar.gzatlas at this save
files changed8 files
VERSION+1 −1app/src/main/java/com/peachpasswords/credentials/CredentialProviderUnlockActivity.kt+8 −1app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialEntryFactory.kt+4 −1app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt+15 −4app/src/main/java/com/peachpasswords/passkeys/PasskeyUnlockIntent.kt+1 −1app/src/main/java/com/peachpasswords/passkeys/PasskeyVerificationRoute.kt+2 −0app/src/main/java/com/peachpasswords/passkeys/PeachCredentialProviderService.kt+1 −0app/src/test/java/com/peachpasswords/passkeys/CredentialProviderContractTest.kt+32 −3app/src/main/java/com/peachpasswords/credentials/CredentialProviderUnlockActivity.kt+8 −1
@@ -127,11 +127,13 @@127127 pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(128128 this,129129 candidate.entryId,130+ verifiedByUnlock = true,130131 ),131132 option = option,132133 displayName = candidate.displayName,133134 lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),134135 icon = icon,136+ requireCredentialManagerVerification = false,135137 )136138 }137139 else -> emptyList<CredentialEntry>()@@ -145,7 +147,11 @@145147 builder.addAction(146148 Action(147149 "Choose another Peach passkey",148- PasskeyCredentialSelectionActivity.choosePendingIntent(this, request),150+ PasskeyCredentialSelectionActivity.choosePendingIntent(151+ this,152+ request,153+ verifiedByUnlock = true,154+ ),149155 "Search eligible passkeys",150156 ),151157 )@@ -154,6 +160,7 @@154160 result,155161 builder.build(),156162 )163+ vault.expectTransientBackground()157164 setResult(RESULT_OK, result)158165 finish()159166 }app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialEntryFactory.kt+4 −1
@@ -20,13 +20,16 @@2020 pendingIntent: PendingIntent,2121 option: BeginGetPublicKeyCredentialOption,2222 icon: Icon,23+ requireCredentialManagerVerification: Boolean,2324 ): PublicKeyCredentialEntry {2425 val builder = PublicKeyCredentialEntry.Builder(context, username, pendingIntent, option)2526 .setDisplayName(displayName)2627 .setLastUsedTime(lastUsedTime)2728 .setIcon(icon)2829 .setAutoSelectAllowed(false)29- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {30+ if (requireCredentialManagerVerification &&31+ Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM32+ ) {3033 builder.setBiometricPromptData(3134 BiometricPromptData(3235 cryptoObject = null,app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt+15 −4
@@ -115,6 +115,7 @@115115 }116116 val biometricResult = providerRequest.biometricPromptResult117117 val route = passkeyVerificationRoute(118+ verifiedByUnlock = intent.getBooleanExtra(EXTRA_VERIFIED_BY_UNLOCK, false),118119 hasEmbeddedResult = biometricResult != null,119120 embeddedSucceeded = biometricResult?.isSuccessful == true,120121 )@@ -212,7 +213,11 @@212213 Column(213214 Modifier.fillMaxWidth().clickable {214215 lifecycleScope.launch {215- val userVerified = runUserVerificationPrompt()216+ val verifiedByUnlock = intent.getBooleanExtra(217+ EXTRA_VERIFIED_BY_UNLOCK,218+ false,219+ )220+ val userVerified = verifiedByUnlock || runUserVerificationPrompt()216221 if (!userVerified) {217222 Log.w(TAG, "flow=manual stage=verification_incomplete")218223 failGet("User verification is required but could not be completed.")@@ -448,6 +453,8 @@448453 449454 companion object {450455 private const val EXTRA_ENTRY_ID = "com.peachpasswords.passkeys.EXTRA_ENTRY_ID"456+ private const val EXTRA_VERIFIED_BY_UNLOCK =457+ "com.peachpasswords.passkeys.EXTRA_VERIFIED_BY_UNLOCK"451458 private const val EXTRA_REQUEST_JSON = "com.peachpasswords.passkeys.EXTRA_REQUEST_JSON"452459 private const val EXTRA_MODE = "com.peachpasswords.passkeys.EXTRA_MODE"453460 private const val EXTRA_BEGIN_REQUEST = "com.peachpasswords.passkeys.EXTRA_BEGIN_REQUEST"@@ -462,15 +469,17 @@462469 fun pendingIntent(463470 context: Context,464471 entryId: String,472+ verifiedByUnlock: Boolean = false,465473 ): PendingIntent {466474 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)467475 .putExtra(EXTRA_ENTRY_ID, entryId)476+ .putExtra(EXTRA_VERIFIED_BY_UNLOCK, verifiedByUnlock)468477 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0469478 return PendingIntent.getActivity(470479 context,471480 PasskeyPendingIntentIds.next(),472481 intent,473- PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,482+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT or mutableFlag,474483 )475484 }476485 @@ -483,23 +492,25 @@483492 context,484493 PasskeyPendingIntentIds.next(),485494 intent,486- PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,495+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT or mutableFlag,487496 )488497 }489498 490499 fun choosePendingIntent(491500 context: Context,492501 request: BeginGetCredentialRequest,502+ verifiedByUnlock: Boolean = false,493503 ): PendingIntent {494504 val intent = Intent(context, PasskeyCredentialSelectionActivity::class.java)495505 .putExtra(EXTRA_MODE, MODE_CHOOSE)496506 .putExtra(EXTRA_BEGIN_REQUEST, BeginGetCredentialRequest.asBundle(request))507+ .putExtra(EXTRA_VERIFIED_BY_UNLOCK, verifiedByUnlock)497508 val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0498509 return PendingIntent.getActivity(499510 context,500511 PasskeyPendingIntentIds.next(),501512 intent,502- PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag,513+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT or mutableFlag,503514 )504515 }505516 }app/src/main/java/com/peachpasswords/passkeys/PasskeyUnlockIntent.kt+1 −1
@@ -15,7 +15,7 @@1515 context,1616 PasskeyPendingIntentIds.next(),1717 intent,18- PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag(),18+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_ONE_SHOT or mutableFlag(),1919 )2020 }2121 app/src/main/java/com/peachpasswords/passkeys/PasskeyVerificationRoute.kt+2 −0
@@ -3,9 +3,11 @@33 internal enum class PasskeyVerificationRoute { ACCEPT, REJECT, PROMPT_IN_PEACH }44 55 internal fun passkeyVerificationRoute(6+ verifiedByUnlock: Boolean,67 hasEmbeddedResult: Boolean,78 embeddedSucceeded: Boolean,89 ): PasskeyVerificationRoute = when {10+ verifiedByUnlock -> PasskeyVerificationRoute.ACCEPT911 !hasEmbeddedResult -> PasskeyVerificationRoute.PROMPT_IN_PEACH1012 embeddedSucceeded -> PasskeyVerificationRoute.ACCEPT1113 else -> PasskeyVerificationRoute.REJECTapp/src/main/java/com/peachpasswords/passkeys/PeachCredentialProviderService.kt+1 −0
@@ -123,6 +123,7 @@123123 displayName = candidate.displayName,124124 lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),125125 icon = icon,126+ requireCredentialManagerVerification = true,126127 )127128 }128129 }app/src/test/java/com/peachpasswords/passkeys/CredentialProviderContractTest.kt+32 −3
@@ -56,16 +56,45 @@5656 fun credentialManagerVerificationResultHasOneOwner() {5757 assertEquals(5858 PasskeyVerificationRoute.ACCEPT,59- passkeyVerificationRoute(hasEmbeddedResult = true, embeddedSucceeded = true),59+ passkeyVerificationRoute(60+ verifiedByUnlock = false,61+ hasEmbeddedResult = true,62+ embeddedSucceeded = true,63+ ),6064 )6165 assertEquals(6266 PasskeyVerificationRoute.REJECT,63- passkeyVerificationRoute(hasEmbeddedResult = true, embeddedSucceeded = false),67+ passkeyVerificationRoute(68+ verifiedByUnlock = false,69+ hasEmbeddedResult = true,70+ embeddedSucceeded = false,71+ ),6472 )6573 assertEquals(6674 PasskeyVerificationRoute.PROMPT_IN_PEACH,67- passkeyVerificationRoute(hasEmbeddedResult = false, embeddedSucceeded = false),75+ passkeyVerificationRoute(76+ verifiedByUnlock = false,77+ hasEmbeddedResult = false,78+ embeddedSucceeded = false,79+ ),6880 )81+ assertEquals(82+ PasskeyVerificationRoute.ACCEPT,83+ passkeyVerificationRoute(84+ verifiedByUnlock = true,85+ hasEmbeddedResult = false,86+ embeddedSucceeded = false,87+ ),88+ )89+ }90+91+ @Test92+ fun authenticatedUnlockPreservesVaultForExactlyTheCredentialManagerHandoff() {93+ val unlock = source("credentials/CredentialProviderUnlockActivity.kt")94+95+ assertTrue(unlock.contains("vault.expectTransientBackground()"))96+ assertTrue(unlock.contains("verifiedByUnlock = true"))97+ assertTrue(source("passkeys/PasskeyCredentialSelectionActivity.kt").contains("PendingIntent.FLAG_ONE_SHOT"))6998 }7099 71100 @Test