TrustedBrowsers.kt

4.4 KB · raw

1package com.peachpasswords.autofill23import android.content.Context4import android.content.pm.PackageManager5import android.content.pm.verify.domain.DomainVerificationManager6import android.content.pm.verify.domain.DomainVerificationUserState7import android.os.Build8import com.peachpasswords.shared.extractDomain9import java.security.MessageDigest1011/** Browsers allowed to assert a web origin, pinned to their release certificates. */12object TrustedBrowsers {13    private const val GOOGLE_RELEASE_CERT =14        "f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83"15    private const val MOZILLA_RELEASE_CERT =16        "a78b62a5165b4494b2fead9e76a280d22d937fee6251aece599446b2ea319b04"17    private const val BRAVE_RELEASE_CERT =18        "9c2db70513515fdbfbbc585b3edf3d7123d4dc67c94ffd306361c1d79bbf18ac"1920    // Unknown browsers still reach the guarded manual picker; they never receive21    // credentials merely because their package name resembles a supported browser.22    private val EXPECTED_CERTIFICATES = mapOf(23        "com.android.chrome" to setOf(GOOGLE_RELEASE_CERT),24        "com.chrome.beta" to setOf(GOOGLE_RELEASE_CERT),25        "com.chrome.dev" to setOf(GOOGLE_RELEASE_CERT),26        "org.mozilla.firefox" to setOf(MOZILLA_RELEASE_CERT),27        "org.mozilla.firefox_beta" to setOf(MOZILLA_RELEASE_CERT),28        "com.brave.browser" to setOf(BRAVE_RELEASE_CERT),29    )3031    @Suppress("DEPRECATION")32    fun isTrusted(context: Context, packageName: String?): Boolean {33        if (packageName !in EXPECTED_CERTIFICATES) return false34        return runCatching {35            context.packageManager36                .getPackageInfo(packageName!!, PackageManager.GET_SIGNATURES)37                .signatures38                .orEmpty()39                .any { hasExpectedCertificate(packageName, sha256(it.toByteArray())) }40        }.getOrDefault(false)41    }4243    internal fun hasExpectedCertificate(packageName: String?, certificateSha256: String): Boolean =44        certificateSha256.lowercase() in EXPECTED_CERTIFICATES[packageName].orEmpty()4546    private fun sha256(bytes: ByteArray): String =47        MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") {48            (it.toInt() and 0xff).toString(16).padStart(2, '0')49        }50}5152internal fun authenticateAutofillOrigin(context: Context, parsed: ParsedStructure): ParsedStructure {53    val claimedWebDomain = parsed.webDomain54    if (claimedWebDomain != null) {55        val domain = extractDomain(claimedWebDomain)56            ?: return parsed.copy(57                originTrust = AutofillOriginTrust.UNKNOWN,58                verifiedDomains = emptyList(),59            )60        if (TrustedBrowsers.isTrusted(context, parsed.packageName)) {61            return parsed.copy(62                originTrust = AutofillOriginTrust.TRUSTED_WEB,63                verifiedDomains = listOf(domain),64            )65        }66        val appDomains = verifiedAppLinkDomains(context, parsed.packageName)67        return if (domain in appDomains) {68            parsed.copy(69                originTrust = AutofillOriginTrust.TRUSTED_NATIVE,70                verifiedDomains = listOf(domain),71            )72        } else {73            parsed.copy(74                originTrust = AutofillOriginTrust.UNKNOWN,75                verifiedDomains = emptyList(),76            )77        }78    }7980    val appDomains = verifiedAppLinkDomains(context, parsed.packageName)81    return if (appDomains.isEmpty()) {82        parsed.copy(originTrust = AutofillOriginTrust.UNKNOWN, verifiedDomains = emptyList())83    } else {84        parsed.copy(85            originTrust = AutofillOriginTrust.TRUSTED_NATIVE,86            verifiedDomains = appDomains,87        )88    }89}9091/** Uses Android's cached Digital Asset Links verdict; older Android versions fail closed. */92internal fun verifiedAppLinkDomains(context: Context, packageName: String?): List<String> {93    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || packageName.isNullOrBlank()) {94        return emptyList()95    }96    return runCatching {97        val state = context.getSystemService(DomainVerificationManager::class.java)98            ?.getDomainVerificationUserState(packageName)99        state?.hostToStateMap.orEmpty()100            .filterValues { it == DomainVerificationUserState.DOMAIN_STATE_VERIFIED }101            .keys102            .mapNotNull { extractDomain(it.removePrefix("*.")) }103            .distinct()104            .sorted()105    }.getOrDefault(emptyList())106}