package com.peachpasswords.credentials import android.app.PendingIntent import android.content.Context import android.content.Intent import android.os.Build import android.os.Bundle import androidx.credentials.GetCredentialResponse import androidx.credentials.PasswordCredential import androidx.credentials.exceptions.GetCredentialUnsupportedException import androidx.credentials.provider.PendingIntentHandler import androidx.fragment.app.FragmentActivity import androidx.lifecycle.lifecycleScope import com.peachpasswords.BuildConfig import com.peachpasswords.autofill.appLabel import com.peachpasswords.autofill.appLinkDomains import com.peachpasswords.passkeys.PasskeyCallerOrigin import com.peachpasswords.shared.extractDomain import com.peachpasswords.ui.components.QaFlags import com.peachpasswords.ui.components.SecureWindowPolicy import com.peachpasswords.vault.VaultRepository import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.launch /** * Completes a Credential Manager password selection. The vault was already * unlocked when the row was offered (the provider never emits rows while * locked), and the tap is the release boundary — the same posture as autofill * (PEACH-AND-01-002 risk acceptance). * * Defense in depth: the framework-verified caller is re-checked against the * entry before release, so a stale or replayed pending intent cannot pull a * credential for a different caller. Fails closed on any doubt. */ @AndroidEntryPoint class PasswordCredentialSelectionActivity : FragmentActivity() { @Inject lateinit var vault: VaultRepository private var debugSecureLease = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // This window briefly holds credential material on screen. Block // screenshots / recents exactly like the passkey selection surface. if (!BuildConfig.DEBUG) { SecureWindowPolicy.setUserSecure(window, true) } else if (!qaSecureCaptureBypass()) { SecureWindowPolicy.acquire(window) debugSecureLease = true } lifecycleScope.launch { completeSelection() } } override fun onDestroy() { if (debugSecureLease) { SecureWindowPolicy.release(window) debugSecureLease = false } super.onDestroy() } private fun qaSecureCaptureBypass(): Boolean = BuildConfig.DEBUG && ( intent.getBooleanExtra(QaFlags.EXTRA_DISABLE_SECURE_CAPTURE, false) || QaFlags.disableSecureCaptureForQa ) private suspend fun completeSelection() { val result = Intent() val entryId = intent.getStringExtra(EXTRA_ENTRY_ID) val entry = entryId?.let(vault::getEntry) val providerRequest = runCatching { PendingIntentHandler.retrieveProviderGetCredentialRequest(intent) }.getOrNull() val callerMatches = providerRequest?.callingAppInfo?.let { info -> if (entry == null) { false } else { val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this) val webOrigin = runCatching { info.getOrigin(allowlist) }.getOrNull() if (!webOrigin.isNullOrBlank()) { val domain = extractDomain(webOrigin) domain != null && PasswordCredentialPlanner .candidates(listOf(entry), setOf(domain), null) .isNotEmpty() } else { PasswordCredentialPlanner.candidates( listOf(entry), appLinkDomains(this, info.packageName).toSet(), appLabel(this, info.packageName), ).isNotEmpty() } } } ?: false val username = entry?.username ?: entry?.email val password = entry?.password if (entry == null || entry.deletedAt != null || !callerMatches || username.isNullOrBlank() || password.isNullOrEmpty() ) { PendingIntentHandler.setGetCredentialException( result, GetCredentialUnsupportedException("Peach cannot fill this login."), ) } else { runCatching { vault.updateEntry(entry.copy(lastUsedAt = System.currentTimeMillis())) } PendingIntentHandler.setGetCredentialResponse( result, GetCredentialResponse(PasswordCredential(username, password)), ) } setResult(RESULT_OK, result) finish() } companion object { private const val EXTRA_ENTRY_ID = "com.peachpasswords.credentials.EXTRA_ENTRY_ID" fun pendingIntent(context: Context, entryId: String): PendingIntent { val intent = Intent(context, PasswordCredentialSelectionActivity::class.java) .putExtra(EXTRA_ENTRY_ID, entryId) val mutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0 return PendingIntent.getActivity( context, "cm-password:$entryId".hashCode(), intent, PendingIntent.FLAG_UPDATE_CURRENT or mutableFlag, ) } } }