1package com.peachpasswords.autofill23import android.app.assist.AssistStructure4import android.text.InputType5import android.view.View6import android.view.autofill.AutofillId78enum class AutofillOriginTrust { TRUSTED_WEB, TRUSTED_NATIVE, UNKNOWN }91011121314data class ParsedStructure(15 val usernameId: AutofillId? = null,16 val emailId: AutofillId? = null,17 val passwordId: AutofillId? = null,18 val otpId: AutofillId? = null,19 val usernameValue: String? = null,20 val emailValue: String? = null,21 val passwordValue: String? = null,22 val otpValue: String? = null,23 val passwordPolicy: PasswordPolicy? = null,24 val isNewPassword: Boolean = false,25 val webDomain: String? = null,26 val packageName: String? = null,27 val originTrust: AutofillOriginTrust = AutofillOriginTrust.UNKNOWN,28 val verifiedDomains: List<String> = emptyList(),29 val cardNumberId: AutofillId? = null,30 val cardExpMonthId: AutofillId? = null,31 val cardExpYearId: AutofillId? = null,32 val cardExpDateId: AutofillId? = null,33 val cardCvcId: AutofillId? = null,34 val cardNameId: AutofillId? = null,35) {36 37 val identityId: AutofillId? get() = emailId ?: usernameId38 val hasUsableField: Boolean get() = passwordId != null || identityId != null || otpId != null || hasCardField39 val hasCardField: Boolean get() = cardNumberId != null || cardExpMonthId != null ||40 cardExpYearId != null || cardExpDateId != null || cardCvcId != null || cardNameId != null41 42 val origin: String? get() = webDomain ?: packageName43}4445464748495051object AssistStructureParser {5253 fun parse(structure: AssistStructure): ParsedStructure {54 55 56 var result = ParsedStructure(57 packageName = authenticatedPackageName(structure.activityComponent?.packageName),58 )59 val windows = structure.windowNodeCount60 for (i in 0 until windows) {61 val root = structure.getWindowNodeAt(i).rootViewNode62 result = traverse(root, result)63 }64 return result65 }6667 private fun traverse(node: AssistStructure.ViewNode, acc: ParsedStructure): ParsedStructure {68 var result = acc69 if (result.webDomain == null && !node.webDomain.isNullOrBlank()) {70 result = result.copy(webDomain = node.webDomain)71 }7273 val id = node.autofillId74 if (id != null && isFillable(node)) {75 val value = node.autofillValue?.takeIf { it.isText }?.textValue?.toString()76 ?: node.text?.toString()77 result = when (classify(node)) {78 FieldKind.PASSWORD -> if (result.passwordId == null) {79 val attrs = htmlAttributes(node)80 result.copy(81 passwordId = id,82 passwordValue = value,83 passwordPolicy = PasswordPolicy.fromHtmlAttributes(attrs)84 .takeIf { it.minLength != null || it.maxLength != null },85 isNewPassword = attrs["autocomplete"]86 ?.contains("new-password", ignoreCase = true) == true,87 )88 } else result89 FieldKind.USERNAME -> if (result.usernameId == null) result.copy(usernameId = id, usernameValue = value) else result90 FieldKind.EMAIL -> if (result.emailId == null) result.copy(emailId = id, emailValue = value) else result91 FieldKind.OTP -> if (result.otpId == null) result.copy(otpId = id, otpValue = value) else result92 FieldKind.CARD_NUMBER -> if (result.cardNumberId == null) result.copy(cardNumberId = id) else result93 FieldKind.CARD_EXP_MONTH -> if (result.cardExpMonthId == null) result.copy(cardExpMonthId = id) else result94 FieldKind.CARD_EXP_YEAR -> if (result.cardExpYearId == null) result.copy(cardExpYearId = id) else result95 FieldKind.CARD_EXP_DATE -> if (result.cardExpDateId == null) result.copy(cardExpDateId = id) else result96 FieldKind.CARD_CVC -> if (result.cardCvcId == null) result.copy(cardCvcId = id) else result97 FieldKind.CARD_NAME -> if (result.cardNameId == null) result.copy(cardNameId = id) else result98 FieldKind.NONE -> result99 }100 }101102 for (i in 0 until node.childCount) {103 result = traverse(node.getChildAt(i), result)104 }105 return result106 }107108 private fun isFillable(node: AssistStructure.ViewNode): Boolean =109 node.autofillType == View.AUTOFILL_TYPE_TEXT &&110 node.visibility == View.VISIBLE111112 113114115116117 private fun htmlAttributes(node: AssistStructure.ViewNode): Map<String, String> {118 val info = node.htmlInfo ?: return emptyMap()119 val out = mutableMapOf<String, String>()120 for (attr in info.attributes.orEmpty()) {121 val key = attr.first?.lowercase() ?: continue122 val value = attr.second ?: continue123 out[key] = value124 }125 return out126 }127128 private enum class FieldKind { USERNAME, EMAIL, PASSWORD, OTP, CARD_NUMBER, CARD_EXP_MONTH, CARD_EXP_YEAR, CARD_EXP_DATE, CARD_CVC, CARD_NAME, NONE }129130 private fun classify(node: AssistStructure.ViewNode): FieldKind {131 node.autofillHints?.forEach { hint ->132 when (hint.lowercase()) {133 View.AUTOFILL_HINT_PASSWORD, "password" -> return FieldKind.PASSWORD134 View.AUTOFILL_HINT_USERNAME, "username" -> return FieldKind.USERNAME135 View.AUTOFILL_HINT_EMAIL_ADDRESS, "email", "emailaddress" -> return FieldKind.EMAIL136 "otp", "smsotpcode", "2facode" -> return FieldKind.OTP137 View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, "creditcardnumber", "ccnumber" -> return FieldKind.CARD_NUMBER138 View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, "creditcardexpirationmonth" -> return FieldKind.CARD_EXP_MONTH139 View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, "creditcardexpirationyear" -> return FieldKind.CARD_EXP_YEAR140 View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, "creditcardexpirationdate" -> return FieldKind.CARD_EXP_DATE141 View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE, "creditcardsecuritycode", "ccsecuritycode" -> return FieldKind.CARD_CVC142 }143 }144145 val type = node.inputType146 val isPasswordType = type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_PASSWORD ||147 type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ||148 type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_NUMBER_VARIATION_PASSWORD149 if (isPasswordType) return FieldKind.PASSWORD150151 val haystack = listOfNotNull(node.idEntry, node.hint, node.text?.toString())152 .joinToString(" ").lowercase()153 return when {154 haystack.isBlank() -> FieldKind.NONE155 Regex("\\b(otp|2fa|mfa|one[- ]?time|verification code|auth code)\\b").containsMatchIn(haystack) -> FieldKind.OTP156 haystack.contains("password") || haystack.contains("passwd") || haystack.contains("pwd") -> FieldKind.PASSWORD157 haystack.contains("email") || haystack.contains("e-mail") -> FieldKind.EMAIL158 haystack.contains("user") || haystack.contains("login") || haystack.contains("account") -> FieldKind.USERNAME159 CARD_NUMBER_RX.containsMatchIn(haystack) -> FieldKind.CARD_NUMBER160 CARD_EXP_MONTH_RX.containsMatchIn(haystack) -> FieldKind.CARD_EXP_MONTH161 CARD_EXP_YEAR_RX.containsMatchIn(haystack) -> FieldKind.CARD_EXP_YEAR162 CARD_EXP_DATE_RX.containsMatchIn(haystack) -> FieldKind.CARD_EXP_DATE163 CARD_CVC_RX.containsMatchIn(haystack) -> FieldKind.CARD_CVC164 CARD_NAME_RX.containsMatchIn(haystack) -> FieldKind.CARD_NAME165 else -> FieldKind.NONE166 }167 }168169 private val CARD_NUMBER_RX = Regex("\\b(card\\s*number|credit\\s*card|card\\s*no|pan|cc\\s*num)\\b")170 private val CARD_EXP_MONTH_RX = Regex("\\b(exp\\s*month|expiration\\s*month|expiry\\s*month)\\b")171 private val CARD_EXP_YEAR_RX = Regex("\\b(exp\\s*year|expiration\\s*year|expiry\\s*year)\\b")172 private val CARD_EXP_DATE_RX = Regex("\\b(exp\\s*date|expiration\\s*date|expiry\\s*date|exp\\b)")173 private val CARD_CVC_RX = Regex("\\b(cvc|cvv|security\\s*code|card\\s*code)\\b")174 private val CARD_NAME_RX = Regex("\\b(cardholder|card\\s*holder|name\\s*on\\s*card|card\\s*name)\\b")175}176177178179180181182internal fun authenticatedPackageName(packageName: String?): String? =183 packageName?.takeIf { it.isNotBlank() }