CredentialProviderUnlockActivity.kt

7.1 KB · raw

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                    allowedUserIds = option.allowedUserIds,106                ).map { candidate ->107                    PasswordCredentialEntry.Builder(108                        this,109                        candidate.username,110                        PasswordCredentialSelectionActivity.pendingIntent(this, candidate.entryId),111                        option,112                    )113                        .setDisplayName(candidate.title)114                        .setLastUsedTime(Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L))115                        .setIcon(icon)116                        .setAutoSelectAllowed(false)117                        .build()118                }119                is BeginGetPublicKeyCredentialOption -> PasskeyCredentialPlanner.candidates(120                    vault.getEntries(),121                    option.requestJson,122                    callerOrigin,123                ).map { candidate ->124                    PasskeyCredentialEntryFactory.create(125                        context = this,126                        username = candidate.username,127                        pendingIntent = PasskeyCredentialSelectionActivity.pendingIntent(128                            this,129                            candidate.entryId,130                            verifiedByUnlock = true,131                        ),132                        option = option,133                        displayName = candidate.displayName,134                        lastUsedTime = Instant.ofEpochMilli(candidate.lastUsedAt ?: 0L),135                        icon = icon,136                        requireCredentialManagerVerification = false,137                    )138                }139                else -> emptyList<CredentialEntry>()140            }141        }142143        val result = Intent()144        val hasPasskeyEntries = entries.any { it is PublicKeyCredentialEntry }145        val builder = BeginGetCredentialResponse.Builder().setCredentialEntries(entries)146        if (hasPasskeyEntries) {147            builder.addAction(148                Action(149                    "Choose another Peach passkey",150                    PasskeyCredentialSelectionActivity.choosePendingIntent(151                        this,152                        request,153                        verifiedByUnlock = true,154                    ),155                    "Search eligible passkeys",156                ),157            )158        }159        PendingIntentHandler.setBeginGetCredentialResponse(160            result,161            builder.build(),162        )163        vault.expectTransientBackground()164        setResult(RESULT_OK, result)165        finish()166    }167}