1package com.peachpasswords.credentials23import android.content.Intent4import android.graphics.drawable.Icon5import android.os.Bundle6import android.util.Log7import com.peachpasswords.BuildConfig8import androidx.activity.compose.setContent9import com.peachpasswords.ui.components.QaFlags10import com.peachpasswords.ui.components.SecureWindowPolicy11import androidx.credentials.provider.BeginGetCredentialRequest12import androidx.credentials.provider.BeginGetPasswordOption13import androidx.credentials.provider.BeginGetPublicKeyCredentialOption14import androidx.credentials.provider.BeginGetCredentialResponse15import androidx.credentials.provider.Action16import androidx.credentials.provider.CredentialEntry17import androidx.credentials.provider.PasswordCredentialEntry18import androidx.credentials.provider.PendingIntentHandler19import androidx.credentials.provider.PublicKeyCredentialEntry20import androidx.fragment.app.FragmentActivity21import com.peachpasswords.R22import com.peachpasswords.passkeys.PasskeyCallerOrigin23import com.peachpasswords.passkeys.PasskeyCredentialPlanner24import com.peachpasswords.passkeys.PasskeyCredentialEntryFactory25import com.peachpasswords.passkeys.PasskeyCredentialSelectionActivity26import com.peachpasswords.passkeys.PasskeyUnlockIntent27import com.peachpasswords.autofill.appLinkDomains28import com.peachpasswords.shared.extractDomain29import com.peachpasswords.ui.lock.LockScreen30import com.peachpasswords.ui.theme.ACCENT_COLORS31import com.peachpasswords.ui.theme.DEFAULT_ACCENT32import com.peachpasswords.ui.theme.PeachTheme33import com.peachpasswords.vault.VaultRepository34import dagger.hilt.android.AndroidEntryPoint35import java.time.Instant36import javax.inject.Inject3738@AndroidEntryPoint39class CredentialProviderUnlockActivity : FragmentActivity() {40 @Inject lateinit var vault: VaultRepository41 private var debugSecureLease = false4243 override fun onCreate(savedInstanceState: Bundle?) {44 super.onCreate(savedInstanceState)45 if (!BuildConfig.DEBUG) SecureWindowPolicy.setUserSecure(window, true)46 else if (!intent.getBooleanExtra(QaFlags.EXTRA_DISABLE_SECURE_CAPTURE, false)) {47 SecureWindowPolicy.acquire(window)48 debugSecureLease = true49 }50 val request = runCatching {51 PendingIntentHandler.retrieveBeginGetCredentialRequest(intent)52 }.getOrNull() ?: intent.getBundleExtra(PasskeyUnlockIntent.EXTRA_REQUEST)53 ?.let(BeginGetCredentialRequest::fromBundle)54 if (request == null) {55 val result = Intent()56 PendingIntentHandler.setGetCredentialException(57 result,58 androidx.credentials.exceptions.GetCredentialUnsupportedException(59 "Peach did not receive the unlock request from Android.",60 ),61 )62 Log.w("PeachPasskey", "flow=unlock stage=begin_request_invalid")63 setResult(RESULT_OK, result)64 finish()65 return66 }67 val settings = vault.settings.value68 val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT69 setContent {70 PeachTheme(accent = accent, textSize = settings.textSize) {71 LockScreen(72 onUnlocked = { complete(request) },73 onNavigate = { finish() },74 )75 }76 }77 }7879 override fun onDestroy() {80 if (debugSecureLease) SecureWindowPolicy.release(window)81 super.onDestroy()82 }8384 private fun complete(request: BeginGetCredentialRequest) {85 Log.i("PeachPasskey", "flow=unlock stage=response_build_started")86 val info = request.callingAppInfo87 val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)88 val webOrigin = info?.let { runCatching { it.getOrigin(allowlist) }.getOrNull() }89 val domains = if (!webOrigin.isNullOrBlank()) {90 setOfNotNull(extractDomain(webOrigin))91 } else {92 info?.let { appLinkDomains(this, it.packageName).toSet() }.orEmpty()93 }94 val packageName = info?.packageName?.takeIf { webOrigin.isNullOrBlank() }95 val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark)96 val callerOrigin = info?.let {97 PasskeyCallerOrigin.resolve(it, allowlist)98 }99 val entries = request.beginGetCredentialOptions.flatMap { option ->100 when (option) {101 is BeginGetPasswordOption -> PasswordCredentialPlanner.candidates(102 vault.getEntries(),103 domains,104 packageName = packageName,105 signerDigests = info?.let { PasskeyCallerOrigin.signerFingerprints(this, it.packageName) }.orEmpty(),106 allowedUserIds = option.allowedUserIds,107 ).map { candidate ->108 PasswordCredentialEntry.Builder(109 this,110 candidate.username,111 PasswordCredentialSelectionActivity.pendingIntent(this, candidate.entryId),112 option,113 )114 .setDisplayName(candidate.title)115 .setLastUsedTime(Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L))116 .setIcon(icon)117 .setAutoSelectAllowed(false)118 .build()119 }120 is BeginGetPublicKeyCredentialOption -> PasskeyCredentialPlanner.candidates(121 vault.getEntries(),122 option.requestJson,123 callerOrigin,124 ).map { candidate ->125 PasskeyCredentialEntryFactory.create(126 context = this,127 username = candidate.username,128 pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(129 this,130 candidate.entryId,131 verifiedByUnlock = true,132 ),133 option = option,134 displayName = candidate.displayName,135 lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),136 icon = icon,137 requireCredentialManagerVerification = false,138 )139 }140 else -> emptyList<CredentialEntry>()141 }142 }143144 val result = Intent()145 val hasPasskeyEntries = entries.any { it is PublicKeyCredentialEntry }146 val builder = BeginGetCredentialResponse.Builder().setCredentialEntries(entries)147 if (hasPasskeyEntries) {148 builder.addAction(149 Action(150 "Choose another Peach passkey",151 PasskeyCredentialSelectionActivity.choosePendingIntent(152 this,153 request,154 verifiedByUnlock = true,155 ),156 "Search eligible passkeys",157 ),158 )159 }160 PendingIntentHandler.setBeginGetCredentialResponse(161 result,162 builder.build(),163 )164 vault.expectTransientBackground()165 setResult(RESULT_OK, result)166 finish()167 }168}