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
↓ peach-android-97fe1c42.tar.gzatlas at this save
files changed6 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 −0app/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. -->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+}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 }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 ),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")