AutofillOrigin.kt

3.9 KB · raw

1package com.peachpasswords.autofill23import android.content.Context4import android.content.pm.verify.domain.DomainVerificationManager5import android.content.pm.verify.domain.DomainVerificationUserState6import android.os.Build7import com.peachpasswords.shared.extractDomain8import com.peachpasswords.passkeys.PasskeyCallerOrigin9import com.peachpasswords.vault.VaultEntry1011/**12 * Origin resolution for autofill matching.13 *14 * The caller package comes from the framework-owned activity component. A raw15 * ViewNode.webDomain is only accepted when that package is a signed browser in16 * the same allowlist used by passkeys; otherwise the request fails closed.17 */1819/**20 * Populate [ParsedStructure.originDomains] and [ParsedStructure.appTitle] from21 * the authenticated origin. Verified browsers match on the page domain; native22 * apps match on their app-link domains and display label. Unverified web claims23 * produce no origin matches.24 */25internal fun resolveAutofillOrigin(context: Context, parsed: ParsedStructure): ParsedStructure {26    if (parsed.webDomain != null) {27        return applyWebOriginVerification(28            parsed,29            PasskeyCallerOrigin.isVerifiedBrowser(context, parsed.packageName),30        )31    }32    return parsed.copy(33        originDomains = appLinkDomains(context, parsed.packageName),34        appTitle = appLabel(context, parsed.packageName),35    )36}3738/** Preserve an unverified web claim as a deny decision instead of reinterpreting it as native. */39internal fun applyWebOriginVerification(40    parsed: ParsedStructure,41    verifiedBrowser: Boolean,42): ParsedStructure {43    val webDomain = parsed.webDomain ?: return parsed44    return if (verifiedBrowser) {45        parsed.copy(46            originDomains = listOfNotNull(extractDomain(webDomain)),47            appTitle = null,48            credentialReleaseAllowed = true,49        )50    } else {51        parsed.copy(52            webDomain = null,53            originDomains = emptyList(),54            appTitle = null,55            credentialReleaseAllowed = false,56        )57    }58}5960/** The display label of the calling app, for matching entries by app title. */61internal fun appLabel(context: Context, packageName: String?): String? {62    if (packageName.isNullOrBlank()) return null63    return runCatching {64        context.packageManager.getApplicationLabel(65            context.packageManager.getApplicationInfo(packageName, 0),66        ).toString().takeIf { it.isNotBlank() }67    }.getOrNull()68}6970/** A native app's verified app-link domains (Android's cached Digital Asset Links verdict). */71internal fun appLinkDomains(context: Context, packageName: String?): List<String> {72    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || packageName.isNullOrBlank()) {73        return emptyList()74    }75    return runCatching {76        val state = context.getSystemService(DomainVerificationManager::class.java)77            ?.getDomainVerificationUserState(packageName)78        state?.hostToStateMap.orEmpty()79            .filterValues { it == DomainVerificationUserState.DOMAIN_STATE_VERIFIED }80            .keys81            .mapNotNull { extractDomain(it.removePrefix("*.")) }82            .distinct()83            .sorted()84    }.getOrDefault(emptyList())85}8687/**88 * Domains to offer to "remember" when the user manually picks [entry] for this89 * origin (Android counterpart of the browser's remember-this-site toast).90 * Mirrors the browser trigger: only offer when the entry already carries at91 * least one known domain and none of them cover this origin. Empty = no offer.92 */93internal fun rememberOfferDomains(entry: VaultEntry, parsed: ParsedStructure): List<String> {94    val existing = sequenceOf(entry.url)95        .plus(entry.additionalUrls.orEmpty().asSequence())96        .filterNotNull()97        .mapNotNull(::extractDomain)98        .toSet()99    if (existing.isEmpty() || parsed.originDomains.isEmpty()) return emptyList()100    return parsed.originDomains.filter { it !in existing }101}