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.MessageDigest10111213141516171819object TrustedBrowsers {20 private enum class PinProvenance { UNCONFIRMED, CONFIRMED }2122 private data class BrowserCertificatePins(23 val hashes: Set<String>,24 val provenance: PinProvenance,25 )2627 private const val GOOGLE_RELEASE_CERT =28 "f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83"29 private const val MOZILLA_RELEASE_CERT =30 "a78b62a5165b4494b2fead9e76a280d22d937fee6251aece599446b2ea319b04"31 private const val BRAVE_RELEASE_CERT =32 "9c2db70513515fdbfbbc585b3edf3d7123d4dc67c94ffd306361c1d79bbf18ac"3334 35 36 37 38 private val CERTIFICATE_REGISTRY = mapOf(39 "com.android.chrome" to BrowserCertificatePins(40 hashes = setOf(GOOGLE_RELEASE_CERT),41 provenance = PinProvenance.UNCONFIRMED,42 ),43 "com.chrome.beta" to BrowserCertificatePins(44 hashes = setOf(GOOGLE_RELEASE_CERT),45 provenance = PinProvenance.UNCONFIRMED,46 ),47 "com.chrome.dev" to BrowserCertificatePins(48 hashes = setOf(GOOGLE_RELEASE_CERT),49 provenance = PinProvenance.UNCONFIRMED,50 ),51 "org.mozilla.firefox" to BrowserCertificatePins(52 hashes = setOf(MOZILLA_RELEASE_CERT),53 provenance = PinProvenance.UNCONFIRMED,54 ),55 "org.mozilla.firefox_beta" to BrowserCertificatePins(56 hashes = setOf(MOZILLA_RELEASE_CERT),57 provenance = PinProvenance.UNCONFIRMED,58 ),59 "com.brave.browser" to BrowserCertificatePins(60 hashes = setOf(BRAVE_RELEASE_CERT),61 provenance = PinProvenance.UNCONFIRMED,62 ),63 )6465 fun isTrusted(context: Context, packageName: String?): Boolean {66 val packageName = packageName ?: return false67 val pins = CERTIFICATE_REGISTRY[packageName] ?: return false68 69 70 71 if (pins.provenance != PinProvenance.CONFIRMED) return false72 return runCatching {73 val info = packageInfo(context.packageManager, packageName)74 ?: return@runCatching false75 val current = mutableSetOf<String>()76 val history = mutableSetOf<String>()77 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {78 val signingInfo = info.signingInfo ?: return@runCatching false79 signingInfo.apkContentsSigners.orEmpty()80 .mapTo(current) { sha256(it.toByteArray()) }81 signingInfo.signingCertificateHistory.orEmpty()82 .mapTo(history) { sha256(it.toByteArray()) }83 } else {84 @Suppress("DEPRECATION")85 info.signatures.orEmpty().mapTo(current) { sha256(it.toByteArray()) }86 }87 certificateSetMatches(88 expected = pins.hashes,89 current = current,90 history = history,91 apiLevel = Build.VERSION.SDK_INT,92 )93 }.getOrDefault(false)94 }9596 private fun packageInfo(packageManager: PackageManager, packageName: String) =97 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {98 packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNING_CERTIFICATES)99 } else {100 @Suppress("DEPRECATION")101 packageManager.getPackageInfo(packageName, PackageManager.GET_SIGNATURES)102 }103104 105106107108109110111112113114115116 internal fun certificateSetMatches(117 expected: Set<String>,118 current: Set<String>,119 history: Set<String>,120 apiLevel: Int,121 ): Boolean {122 if (expected.isEmpty()) return false123 if (current.isEmpty()) return false124 val normalized = expected.map(String::lowercase)125 return current.any { it.lowercase() in normalized }126 }127128 internal fun hasLocallyConfirmedPins(packageName: String?): Boolean =129 CERTIFICATE_REGISTRY[packageName]?.provenance == PinProvenance.CONFIRMED130131 132 internal fun registeredPackages(): Set<String> = CERTIFICATE_REGISTRY.keys133134 135 internal fun allPinsUnconfirmed(): Boolean =136 CERTIFICATE_REGISTRY.values.all { it.provenance == PinProvenance.UNCONFIRMED }137138 internal fun hasExpectedCertificate(packageName: String?, certificateSha256: String): Boolean =139 certificateSha256.lowercase() in CERTIFICATE_REGISTRY[packageName]?.hashes.orEmpty()140141 private fun sha256(bytes: ByteArray): String =142 MessageDigest.getInstance("SHA-256").digest(bytes).joinToString("") {143 (it.toInt() and 0xff).toString(16).padStart(2, '0')144 }145}146147internal fun authenticateAutofillOrigin(context: Context, parsed: ParsedStructure): ParsedStructure {148 val claimedWebDomain = parsed.webDomain149 if (claimedWebDomain != null) {150 val domain = extractDomain(claimedWebDomain)151 ?: return parsed.copy(152 originTrust = AutofillOriginTrust.UNKNOWN,153 verifiedDomains = emptyList(),154 )155 if (TrustedBrowsers.isTrusted(context, parsed.packageName)) {156 return parsed.copy(157 originTrust = AutofillOriginTrust.TRUSTED_WEB,158 verifiedDomains = listOf(domain),159 )160 }161 val appDomains = verifiedAppLinkDomains(context, parsed.packageName)162 return if (domain in appDomains) {163 parsed.copy(164 originTrust = AutofillOriginTrust.TRUSTED_NATIVE,165 verifiedDomains = listOf(domain),166 )167 } else {168 parsed.copy(169 originTrust = AutofillOriginTrust.UNKNOWN,170 verifiedDomains = emptyList(),171 )172 }173 }174175 val appDomains = verifiedAppLinkDomains(context, parsed.packageName)176 return if (appDomains.isEmpty()) {177 parsed.copy(originTrust = AutofillOriginTrust.UNKNOWN, verifiedDomains = emptyList())178 } else {179 parsed.copy(180 originTrust = AutofillOriginTrust.TRUSTED_NATIVE,181 verifiedDomains = appDomains,182 )183 }184}185186187internal fun verifiedAppLinkDomains(context: Context, packageName: String?): List<String> {188 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S || packageName.isNullOrBlank()) {189 return emptyList()190 }191 return runCatching {192 val state = context.getSystemService(DomainVerificationManager::class.java)193 ?.getDomainVerificationUserState(packageName)194 state?.hostToStateMap.orEmpty()195 .filterValues { it == DomainVerificationUserState.DOMAIN_STATE_VERIFIED }196 .keys197 .mapNotNull { extractDomain(it.removePrefix("*.")) }198 .distinct()199 .sorted()200 }.getOrDefault(emptyList())201}