package com.peachpasswords.autofill import android.content.Context import android.content.pm.PackageManager import android.content.pm.verify.domain.DomainVerificationManager import android.content.pm.verify.domain.DomainVerificationUserState import android.os.Build import com.peachpasswords.shared.extractDomain import java.security.MessageDigest /** Browsers allowed to assert a web origin, pinned to their release certificates. */ object TrustedBrowsers { private const val GOOGLE_RELEASE_CERT = "f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83" private const val MOZILLA_RELEASE_CERT = "a78b62a5165b4494b2fead9e76a280d22d937fee6251aece599446b2ea319b04" private const val BRAVE_RELEASE_CERT = "9c2db70513515fdbfbbc585b3edf3d7123d4dc67c94ffd306361c1d79bbf18ac" // Unknown browsers still reach the guarded manual picker; they never receive // credentials merely because their package name resembles a supported browser. private val EXPECTED_CERTIFICATES = mapOf( "com.android.chrome" to setOf(GOOGLE_RELEASE_CERT), "com.chrome.beta" to setOf(GOOGLE_RELEASE_CERT), "com.chrome.dev" to setOf(GOOGLE_RELEASE_CERT), "org.mozilla.firefox" to setOf(MOZILLA_RELEASE_CERT), "org.mozilla.firefox_beta" to setOf(MOZILLA_RELEASE_CERT), "com.brave.browser" to setOf(BRAVE_RELEASE_CERT), ) @Suppress("DEPRECATION") fun isTrusted(context: Context, packageName: String?): Boolean { if (packageName !in EXPECTED_CERTIFICATES) return false return runCatching { context.packageManager .getPackageInfo(packageName!!, PackageManager.GET_SIGNATURES) .signatures .orEmpty() .any { hasExpectedCertificate(packageName, sha256(it.toByteArray())) } }.getOrDefault(false) } internal fun hasExpectedCertificate(packageName: String?, certificateSha256: String): Boolean = certificateSha256.lowercase() in EXPECTED_CERTIFICATES[packageName].orEmpty() private fun sha256(bytes: ByteArray): String = MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") { (it.toInt() and 0xff).toString(16).padStart(2, '0') } } internal fun authenticateAutofillOrigin(context: Context, parsed: ParsedStructure): ParsedStructure { val claimedWebDomain = parsed.webDomain if (claimedWebDomain != null) { val domain = extractDomain(claimedWebDomain) ?: return parsed.copy( originTrust = AutofillOriginTrust.UNKNOWN, verifiedDomains = emptyList(), ) if (TrustedBrowsers.isTrusted(context, parsed.packageName)) { return parsed.copy( originTrust = AutofillOriginTrust.TRUSTED_WEB, verifiedDomains = listOf(domain), ) } val appDomains = verifiedAppLinkDomains(context, parsed.packageName) return if (domain in appDomains) { parsed.copy( originTrust = AutofillOriginTrust.TRUSTED_NATIVE, verifiedDomains = listOf(domain), ) } else { parsed.copy( originTrust = AutofillOriginTrust.UNKNOWN, verifiedDomains = emptyList(), ) } } val appDomains = verifiedAppLinkDomains(context, parsed.packageName) return if (appDomains.isEmpty()) { parsed.copy(originTrust = AutofillOriginTrust.UNKNOWN, verifiedDomains = emptyList()) } else { parsed.copy( originTrust = AutofillOriginTrust.TRUSTED_NATIVE, verifiedDomains = appDomains, ) } } /** Uses Android's cached Digital Asset Links verdict; older Android versions fail closed. */ internal fun verifiedAppLinkDomains(context: Context, packageName: String?): List { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || packageName.isNullOrBlank()) { return emptyList() } return runCatching { val state = context.getSystemService(DomainVerificationManager::class.java) ?.getDomainVerificationUserState(packageName) state?.hostToStateMap.orEmpty() .filterValues { it == DomainVerificationUserState.DOMAIN_STATE_VERIFIED } .keys .mapNotNull { extractDomain(it.removePrefix("*.")) } .distinct() .sorted() }.getOrDefault(emptyList()) }