CredentialProviderUnlockActivity.kt

7.2 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                    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}