97fe1c42be52

fix locked Credential Manager auth handoff and return password entries (v2.15.5)

time
2026-09-05 23:19 UTC · 1 month ago
machine
koh2
agent
claude-opus-5
session
credential provider handoff
parent
809d7406f7cc
6 files VERSION+1 −1app/src/main/AndroidManifest.xml+6 −0app/src/main/java/com/peachpasswords/credentials/CredentialProviderUnlockActivity.kt+105 −0app/src/main/java/com/peachpasswords/passkeys/PasskeyUnlockIntent.kt+6 −6app/src/main/java/com/peachpasswords/passkeys/PeachCredentialProviderService.kt+1 −1app/src/test/java/com/peachpasswords/security/SecureFlagCoverageTest.kt+2 −0
VERSION+1 −1
@@ -1 +1 @@1-2.15.41+2.15.5

view file

app/src/main/AndroidManifest.xml+6 −0
@@ -81,6 +81,12 @@8181             android:excludeFromRecents="true"8282             android:theme="@style/Theme.Peach.Transparent" />8383 84+        <activity85+            android:name=".credentials.CredentialProviderUnlockActivity"86+            android:exported="false"87+            android:excludeFromRecents="true"88+            android:theme="@style/Theme.Peach.Transparent" />89+8490         <!-- WorkManager is initialized on demand by PeachApplication (Configuration.Provider)8591              so the update worker can be Hilt-injected; its default auto-initializer would8692              race that and build a factory-less WorkManager first. -->

view file

app/src/main/java/com/peachpasswords/credentials/CredentialProviderUnlockActivity.kt+105 −0
@@ -0,0 +1,105 @@1+package com.peachpasswords.credentials2+3+import android.content.Intent4+import android.graphics.drawable.Icon5+import android.os.Bundle6+import com.peachpasswords.BuildConfig7+import androidx.activity.compose.setContent8+import com.peachpasswords.ui.components.QaFlags9+import com.peachpasswords.ui.components.SecureWindowPolicy10+import androidx.credentials.provider.BeginGetCredentialRequest11+import androidx.credentials.provider.BeginGetPasswordOption12+import androidx.credentials.provider.BeginGetCredentialResponse13+import androidx.credentials.provider.PasswordCredentialEntry14+import androidx.credentials.provider.PendingIntentHandler15+import androidx.fragment.app.FragmentActivity16+import com.peachpasswords.R17+import com.peachpasswords.passkeys.PasskeyCallerOrigin18+import com.peachpasswords.passkeys.PasskeyUnlockIntent19+import com.peachpasswords.autofill.appLinkDomains20+import com.peachpasswords.shared.extractDomain21+import com.peachpasswords.ui.lock.LockScreen22+import com.peachpasswords.ui.theme.ACCENT_COLORS23+import com.peachpasswords.ui.theme.DEFAULT_ACCENT24+import com.peachpasswords.ui.theme.PeachTheme25+import com.peachpasswords.vault.VaultRepository26+import dagger.hilt.android.AndroidEntryPoint27+import java.time.Instant28+import javax.inject.Inject29+30+@AndroidEntryPoint31+class CredentialProviderUnlockActivity : FragmentActivity() {32+    @Inject lateinit var vault: VaultRepository33+    private var debugSecureLease = false34+35+    override fun onCreate(savedInstanceState: Bundle?) {36+        super.onCreate(savedInstanceState)37+        if (!BuildConfig.DEBUG) SecureWindowPolicy.setUserSecure(window, true)38+        else if (!intent.getBooleanExtra(QaFlags.EXTRA_DISABLE_SECURE_CAPTURE, false)) {39+            SecureWindowPolicy.acquire(window)40+            debugSecureLease = true41+        }42+        val request = intent.getBundleExtra(PasskeyUnlockIntent.EXTRA_REQUEST)43+            ?.let(BeginGetCredentialRequest::fromBundle)44+        if (request == null) {45+            finish()46+            return47+        }48+        val settings = vault.settings.value49+        val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT50+        setContent {51+            PeachTheme(accent = accent, textSize = settings.textSize) {52+                LockScreen(53+                    onUnlocked = { complete(request) },54+                    onNavigate = { finish() },55+                )56+            }57+        }58+    }59+60+    override fun onDestroy() {61+        if (debugSecureLease) SecureWindowPolicy.release(window)62+        super.onDestroy()63+    }64+65+    private fun complete(request: BeginGetCredentialRequest) {66+        val info = request.callingAppInfo67+        val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)68+        val webOrigin = info?.let { runCatching { it.getOrigin(allowlist) }.getOrNull() }69+        val domains = if (!webOrigin.isNullOrBlank()) {70+            setOfNotNull(extractDomain(webOrigin))71+        } else {72+            info?.let { appLinkDomains(this, it.packageName).toSet() }.orEmpty()73+        }74+        val packageName = info?.packageName?.takeIf { webOrigin.isNullOrBlank() }75+        val entries = request.beginGetCredentialOptions76+            .filterIsInstance<BeginGetPasswordOption>()77+            .flatMap { option ->78+                PasswordCredentialPlanner.candidates(79+                    vault.getEntries(),80+                    domains,81+                    packageName = packageName,82+                    allowedUserIds = option.allowedUserIds,83+                ).map { candidate ->84+                    PasswordCredentialEntry.Builder(85+                        this,86+                        candidate.username,87+                        PasswordCredentialSelectionActivity.pendingIntent(this, candidate.entryId),88+                        option,89+                    )90+                        .setDisplayName(candidate.title)91+                        .setLastUsedTime(Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L))92+                        .setIcon(Icon.createWithResource(this, R.drawable.ic_peach_mark))93+                        .setAutoSelectAllowed(false)94+                        .build()95+                }96+            }97+        val result = Intent()98+        PendingIntentHandler.setBeginGetCredentialResponse(99+            result,100+            BeginGetCredentialResponse.Builder().setCredentialEntries(entries).build(),101+        )102+        setResult(RESULT_OK, result)103+        finish()104+    }105+}

view file

app/src/main/java/com/peachpasswords/passkeys/PasskeyUnlockIntent.kt+6 −6
@@ -3,14 +3,13 @@33 import android.app.PendingIntent44 import android.content.Context55 import android.content.Intent6-import com.peachpasswords.MainActivity7-import com.peachpasswords.PeachActions6+import androidx.credentials.provider.BeginGetCredentialRequest7+import com.peachpasswords.credentials.CredentialProviderUnlockActivity88 99 object PasskeyUnlockIntent {10-    fun pendingIntent(context: Context): PendingIntent {11-        val intent = Intent(context, MainActivity::class.java)12-            .setAction(PeachActions.PASSKEY_UNLOCK)13-            .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)10+    fun pendingIntent(context: Context, request: BeginGetCredentialRequest): PendingIntent {11+        val intent = Intent(context, CredentialProviderUnlockActivity::class.java)12+            .putExtra(EXTRA_REQUEST, BeginGetCredentialRequest.asBundle(request))1413         return PendingIntent.getActivity(1514             context,1615             REQUEST_CODE,@@ -19,5 +18,6 @@1918         )2019     }2120 21+    const val EXTRA_REQUEST = "com.peachpasswords.passkeys.EXTRA_BEGIN_GET_REQUEST"2222     private const val REQUEST_CODE = 0x504b2323 }

view file

app/src/main/java/com/peachpasswords/passkeys/PeachCredentialProviderService.kt+1 −1
@@ -72,7 +72,7 @@7272                     authenticationActions = listOf(7373                         AuthenticationAction(7474                             title = "Unlock Peach",75-                            pendingIntent = PasskeyUnlockIntent.pendingIntent(this),75+                            pendingIntent = PasskeyUnlockIntent.pendingIntent(this, request),7676                         ),7777                     ),7878                 ),

view file

app/src/test/java/com/peachpasswords/security/SecureFlagCoverageTest.kt+2 −0
@@ -28,6 +28,7 @@2828     private val secretActivities = listOf(2929         "com.peachpasswords.autofill.AutofillEntryPickerActivity",3030         "com.peachpasswords.passkeys.PasskeyCredentialSelectionActivity",31+        "com.peachpasswords.credentials.CredentialProviderUnlockActivity",3132     )3233 3334     @Test fun secret_screens_keep_flag_secure_coverage() {@@ -87,6 +88,7 @@8788             "app/src/main/java/com/peachpasswords/MainActivity.kt",8889             "app/src/main/java/com/peachpasswords/autofill/AutofillEntryPickerActivity.kt",8990             "app/src/main/java/com/peachpasswords/passkeys/PasskeyCredentialSelectionActivity.kt",91+            "app/src/main/java/com/peachpasswords/credentials/CredentialProviderUnlockActivity.kt",9092         ).forEach { relativePath ->9193             val file = java.io.File(relativePath).takeIf { it.exists() }9294                 ?: java.io.File("../$relativePath")

view file