1package com.peachpasswords.credentials23import android.content.Intent4import android.graphics.drawable.Icon5import android.os.Bundle6import com.peachpasswords.BuildConfig7import androidx.activity.compose.setContent8import com.peachpasswords.ui.components.QaFlags9import com.peachpasswords.ui.components.SecureWindowPolicy10import androidx.credentials.provider.BeginGetCredentialRequest11import androidx.credentials.provider.BeginGetPasswordOption12import androidx.credentials.provider.BeginGetPublicKeyCredentialOption13import androidx.credentials.provider.BeginGetCredentialResponse14import androidx.credentials.provider.Action15import androidx.credentials.provider.CredentialEntry16import androidx.credentials.provider.PasswordCredentialEntry17import androidx.credentials.provider.PendingIntentHandler18import androidx.credentials.provider.PublicKeyCredentialEntry19import androidx.fragment.app.FragmentActivity20import com.peachpasswords.R21import com.peachpasswords.passkeys.PasskeyCallerOrigin22import com.peachpasswords.passkeys.PasskeyCredentialPlanner23import com.peachpasswords.passkeys.PasskeyCredentialSelectionActivity24import com.peachpasswords.passkeys.PasskeyUnlockIntent25import com.peachpasswords.autofill.appLinkDomains26import com.peachpasswords.shared.extractDomain27import com.peachpasswords.ui.lock.LockScreen28import com.peachpasswords.ui.theme.ACCENT_COLORS29import com.peachpasswords.ui.theme.DEFAULT_ACCENT30import com.peachpasswords.ui.theme.PeachTheme31import com.peachpasswords.vault.VaultRepository32import dagger.hilt.android.AndroidEntryPoint33import java.time.Instant34import javax.inject.Inject3536@AndroidEntryPoint37class CredentialProviderUnlockActivity : FragmentActivity() {38 @Inject lateinit var vault: VaultRepository39 private var debugSecureLease = false4041 override fun onCreate(savedInstanceState: Bundle?) {42 super.onCreate(savedInstanceState)43 if (!BuildConfig.DEBUG) SecureWindowPolicy.setUserSecure(window, true)44 else if (!intent.getBooleanExtra(QaFlags.EXTRA_DISABLE_SECURE_CAPTURE, false)) {45 SecureWindowPolicy.acquire(window)46 debugSecureLease = true47 }48 val request = intent.getBundleExtra(PasskeyUnlockIntent.EXTRA_REQUEST)49 ?.let(BeginGetCredentialRequest::fromBundle)50 if (request == null) {51 finish()52 return53 }54 val settings = vault.settings.value55 val accent = ACCENT_COLORS[settings.accentColor] ?: DEFAULT_ACCENT56 setContent {57 PeachTheme(accent = accent, textSize = settings.textSize) {58 LockScreen(59 onUnlocked = { complete(request) },60 onNavigate = { finish() },61 )62 }63 }64 }6566 override fun onDestroy() {67 if (debugSecureLease) SecureWindowPolicy.release(window)68 super.onDestroy()69 }7071 private fun complete(request: BeginGetCredentialRequest) {72 val info = request.callingAppInfo73 val allowlist = PasskeyCallerOrigin.privilegedAllowlist(this)74 val webOrigin = info?.let { runCatching { it.getOrigin(allowlist) }.getOrNull() }75 val domains = if (!webOrigin.isNullOrBlank()) {76 setOfNotNull(extractDomain(webOrigin))77 } else {78 info?.let { appLinkDomains(this, it.packageName).toSet() }.orEmpty()79 }80 val packageName = info?.packageName?.takeIf { webOrigin.isNullOrBlank() }81 val icon = Icon.createWithResource(this, R.drawable.ic_peach_mark)82 val callerOrigin = info?.let {83 PasskeyCallerOrigin.resolve(it, allowlist)84 }85 val entries = request.beginGetCredentialOptions.flatMap { option ->86 when (option) {87 is BeginGetPasswordOption -> PasswordCredentialPlanner.candidates(88 vault.getEntries(),89 domains,90 packageName = packageName,91 allowedUserIds = option.allowedUserIds,92 ).map { candidate ->93 PasswordCredentialEntry.Builder(94 this,95 candidate.username,96 PasswordCredentialSelectionActivity.pendingIntent(this, candidate.entryId),97 option,98 )99 .setDisplayName(candidate.title)100 .setLastUsedTime(Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L))101 .setIcon(icon)102 .setAutoSelectAllowed(false)103 .build()104 }105 is BeginGetPublicKeyCredentialOption -> PasskeyCredentialPlanner.candidates(106 vault.getEntries(),107 option.requestJson,108 callerOrigin,109 ).map { candidate ->110 PublicKeyCredentialEntry(111 context = this,112 username = candidate.username,113 pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(114 this,115 candidate.entryId,116 option.requestJson,117 callerOrigin,118 option.clientDataHash,119 ),120 beginGetPublicKeyCredentialOption = option,121 displayName = candidate.displayName,122 lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),123 icon = icon,124 isAutoSelectAllowed = false,125 )126 }127 else -> emptyList<CredentialEntry>()128 }129 }130131 val result = Intent()132 val hasPasskeyEntries = entries.any { it is PublicKeyCredentialEntry }133 val builder = BeginGetCredentialResponse.Builder().setCredentialEntries(entries)134 if (hasPasskeyEntries) {135 builder.addAction(136 Action(137 "Choose another Peach passkey",138 PasskeyCredentialSelectionActivity.choosePendingIntent(this, request, callerOrigin),139 "Search eligible passkeys",140 ),141 )142 }143 PendingIntentHandler.setBeginGetCredentialResponse(144 result,145 builder.build(),146 )147 setResult(RESULT_OK, result)148 finish()149 }150}