package com.peachpasswords.credentials import android.content.Intent import android.graphics.drawable.Icon import android.os.Bundle import android.util.Log import com.peachpasswords.BuildConfig import androidx.activity.compose.setContent import com.peachpasswords.ui.components.QaFlags import com.peachpasswords.ui.components.SecureWindowPolicy import androidx.credentials.provider.BeginGetCredentialRequest import androidx.credentials.provider.BeginGetPasswordOption import androidx.credentials.provider.BeginGetPublicKeyCredentialOption import androidx.credentials.provider.BeginGetCredentialResponse import androidx.credentials.provider.Action import androidx.credentials.provider.CredentialEntry import androidx.credentials.provider.PasswordCredentialEntry import androidx.credentials.provider.PendingIntentHandler import androidx.credentials.provider.PublicKeyCredentialEntry import androidx.fragment.app.FragmentActivity import com.peachpasswords.R import com.peachpasswords.passkeys.PasskeyCallerOrigin import com.peachpasswords.passkeys.PasskeyCredentialPlanner import com.peachpasswords.passkeys.PasskeyCredentialEntryFactory import com.peachpasswords.passkeys.PasskeyCredentialSelectionActivity import com.peachpasswords.passkeys.PasskeyUnlockIntent import com.peachpasswords.autofill.appLinkDomains import com.peachpasswords.shared.extractDomain import com.peachpasswords.ui.lock.LockScreen import com.peachpasswords.ui.theme.ACCENT_COLORS import com.peachpasswords.ui.theme.DEFAULT_ACCENT import com.peachpasswords.ui.theme.PeachTheme import com.peachpasswords.vault.VaultRepository import dagger.hilt.android.AndroidEntryPoint import java.time.Instant import javax.inject.Inject @AndroidEntryPoint class CredentialProviderUnlockActivity : FragmentActivity() { @Inject lateinit var vault: VaultRepository private var debugSecureLease = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (!BuildConfig.DEBUG) SecureWindowPolicy.setUserSecure(window, true) else if (!intent.getBooleanExtra(QaFlags.EXTRA_DISABLE_SECURE_CAPTURE, false)) { SecureWindowPolicy.acquire(window) debugSecureLease = true } val request = runCatching { PendingIntentHandler.retrieveBeginGetCredentialRequest(intent) }.getOrNull() ?: intent.getBundleExtra(PasskeyUnlockIntent.EXTRA_REQUEST) ?.let(BeginGetCredentialRequest::fromBundle) if (request == null) { val result = Intent() PendingIntentHandler.setGetCredentialException( result, androidx.credentials.exceptions.GetCredentialUnsupportedException( "Peach did not receive the unlock request from Android.", ), ) Log.w("PeachPasskey", "flow=unlock stage=begin_request_invalid") setResult(RESULT_OK, result) finish() return } val settings = vault.settings.value val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT setContent { PeachTheme(accent = accent, textSize = settings.textSize) { LockScreen( onUnlocked = { complete(request) }, onNavigate = { finish() }, ) } } } override fun onDestroy() { if (debugSecureLease) SecureWindowPolicy.release(window) super.onDestroy() } private fun complete(request: BeginGetCredentialRequest) { Log.i("PeachPasskey", "flow=unlock stage=response_build_started") val info = request.callingAppInfo val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this) val webOrigin = info?.let { runCatching { it.getOrigin(allowlist) }.getOrNull() } val domains = if (!webOrigin.isNullOrBlank()) { setOfNotNull(extractDomain(webOrigin)) } else { info?.let { appLinkDomains(this, it.packageName).toSet() }.orEmpty() } val packageName = info?.packageName?.takeIf { webOrigin.isNullOrBlank() } val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark) val callerOrigin = info?.let { PasskeyCallerOrigin.resolve(it, allowlist) } val entries = request.beginGetCredentialOptions.flatMap { option -> when (option) { is BeginGetPasswordOption -> PasswordCredentialPlanner.candidates( vault.getEntries(), domains, packageName = packageName, signerDigests = info?.let { PasskeyCallerOrigin.signerFingerprints(this, it.packageName) }.orEmpty(), allowedUserIds = option.allowedUserIds, ).map { candidate -> PasswordCredentialEntry.Builder( this, candidate.username, PasswordCredentialSelectionActivity.pendingIntent(this, candidate.entryId), option, ) .setDisplayName(candidate.title) .setLastUsedTime(Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L)) .setIcon(icon) .setAutoSelectAllowed(false) .build() } is BeginGetPublicKeyCredentialOption -> PasskeyCredentialPlanner.candidates( vault.getEntries(), option.requestJson, callerOrigin, ).map { candidate -> PasskeyCredentialEntryFactory.create( context = this, username = candidate.username, pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent( this, candidate.entryId, verifiedByUnlock = true, ), option = option, displayName = candidate.displayName, lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L), icon = icon, requireCredentialManagerVerification = false, ) } else -> emptyList() } } val result = Intent() val hasPasskeyEntries = entries.any { it is PublicKeyCredentialEntry } val builder = BeginGetCredentialResponse.Builder().setCredentialEntries(entries) if (hasPasskeyEntries) { builder.addAction( Action( "Choose another Peach passkey", PasskeyCredentialSelectionActivity.choosePendingIntent( this, request, verifiedByUnlock = true, ), "Search eligible passkeys", ), ) } PendingIntentHandler.setBeginGetCredentialResponse( result, builder.build(), ) vault.expectTransientBackground() setResult(RESULT_OK, result) finish() } }