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 webDomain: String? = null,24 val packageName: String? = null,25 val originTrust: AutofillOriginTrust = AutofillOriginTrust.UNKNOWN,26 val verifiedDomains: List<String> = emptyList(),27 val cardNumberId: AutofillId? = null,28 val cardExpMonthId: AutofillId? = null,29 val cardExpYearId: AutofillId? = null,30 val cardExpDateId: AutofillId? = null,31 val cardCvcId: AutofillId? = null,32 val cardNameId: AutofillId? = null,33) {34 35 val identityId: AutofillId? get() = emailId ?: usernameId36 val hasUsableField: Boolean get() = passwordId != null || identityId != null || otpId != null || hasCardField37 val hasCardField: Boolean get() = cardNumberId != null || cardExpMonthId != null ||38 cardExpYearId != null || cardExpDateId != null || cardCvcId != null || cardNameId != null39 40 val origin: String? get() = webDomain ?: packageName41}4243444546474849object AssistStructureParser {5051 fun parse(structure: AssistStructure): ParsedStructure {52 53 54 var result = ParsedStructure(55 packageName = authenticatedPackageName(structure.activityComponent?.packageName),56 )57 val windows = structure.windowNodeCount58 for (i in 0 until windows) {59 val root = structure.getWindowNodeAt(i).rootViewNode60 result = traverse(root, result)61 }62 return result63 }6465 private fun traverse(node: AssistStructure.ViewNode, acc: ParsedStructure): ParsedStructure {66 var result = acc67 if (result.webDomain == null && !node.webDomain.isNullOrBlank()) {68 result = result.copy(webDomain = node.webDomain)69 }7071 val id = node.autofillId72 if (id != null && isFillable(node)) {73 val value = node.autofillValue?.takeIf { it.isText }?.textValue?.toString()74 ?: node.text?.toString()75 result = when (classify(node)) {76 FieldKind.PASSWORD -> if (result.passwordId == null) result.copy(passwordId = id, passwordValue = value) else result77 FieldKind.USERNAME -> if (result.usernameId == null) result.copy(usernameId = id, usernameValue = value) else result78 FieldKind.EMAIL -> if (result.emailId == null) result.copy(emailId = id, emailValue = value) else result79 FieldKind.OTP -> if (result.otpId == null) result.copy(otpId = id, otpValue = value) else result80 FieldKind.CARD_NUMBER -> if (result.cardNumberId == null) result.copy(cardNumberId = id) else result81 FieldKind.CARD_EXP_MONTH -> if (result.cardExpMonthId == null) result.copy(cardExpMonthId = id) else result82 FieldKind.CARD_EXP_YEAR -> if (result.cardExpYearId == null) result.copy(cardExpYearId = id) else result83 FieldKind.CARD_EXP_DATE -> if (result.cardExpDateId == null) result.copy(cardExpDateId = id) else result84 FieldKind.CARD_CVC -> if (result.cardCvcId == null) result.copy(cardCvcId = id) else result85 FieldKind.CARD_NAME -> if (result.cardNameId == null) result.copy(cardNameId = id) else result86 FieldKind.NONE -> result87 }88 }8990 for (i in 0 until node.childCount) {91 result = traverse(node.getChildAt(i), result)92 }93 return result94 }9596 private fun isFillable(node: AssistStructure.ViewNode): Boolean =97 node.autofillType == View.AUTOFILL_TYPE_TEXT &&98 node.visibility == View.VISIBLE99100 private enum class FieldKind { USERNAME, EMAIL, PASSWORD, OTP, CARD_NUMBER, CARD_EXP_MONTH, CARD_EXP_YEAR, CARD_EXP_DATE, CARD_CVC, CARD_NAME, NONE }101102 private fun classify(node: AssistStructure.ViewNode): FieldKind {103 node.autofillHints?.forEach { hint ->104 when (hint.lowercase()) {105 View.AUTOFILL_HINT_PASSWORD, "password" -> return FieldKind.PASSWORD106 View.AUTOFILL_HINT_USERNAME, "username" -> return FieldKind.USERNAME107 View.AUTOFILL_HINT_EMAIL_ADDRESS, "email", "emailaddress" -> return FieldKind.EMAIL108 "otp", "smsotpcode", "2facode" -> return FieldKind.OTP109 View.AUTOFILL_HINT_CREDIT_CARD_NUMBER, "creditcardnumber", "ccnumber" -> return FieldKind.CARD_NUMBER110 View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_MONTH, "creditcardexpirationmonth" -> return FieldKind.CARD_EXP_MONTH111 View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_YEAR, "creditcardexpirationyear" -> return FieldKind.CARD_EXP_YEAR112 View.AUTOFILL_HINT_CREDIT_CARD_EXPIRATION_DATE, "creditcardexpirationdate" -> return FieldKind.CARD_EXP_DATE113 View.AUTOFILL_HINT_CREDIT_CARD_SECURITY_CODE, "creditcardsecuritycode", "ccsecuritycode" -> return FieldKind.CARD_CVC114 }115 }116117 val type = node.inputType118 val isPasswordType = type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_PASSWORD ||119 type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD ||120 type and InputType.TYPE_MASK_VARIATION == InputType.TYPE_NUMBER_VARIATION_PASSWORD121 if (isPasswordType) return FieldKind.PASSWORD122123 val haystack = listOfNotNull(node.idEntry, node.hint, node.text?.toString())124 .joinToString(" ").lowercase()125 return when {126 haystack.isBlank() -> FieldKind.NONE127 Regex("\\b(otp|2fa|mfa|one[- ]?time|verification code|auth code)\\b").containsMatchIn(haystack) -> FieldKind.OTP128 haystack.contains("password") || haystack.contains("passwd") || haystack.contains("pwd") -> FieldKind.PASSWORD129 haystack.contains("email") || haystack.contains("e-mail") -> FieldKind.EMAIL130 haystack.contains("user") || haystack.contains("login") || haystack.contains("account") -> FieldKind.USERNAME131 CARD_NUMBER_RX.containsMatchIn(haystack) -> FieldKind.CARD_NUMBER132 CARD_EXP_MONTH_RX.containsMatchIn(haystack) -> FieldKind.CARD_EXP_MONTH133 CARD_EXP_YEAR_RX.containsMatchIn(haystack) -> FieldKind.CARD_EXP_YEAR134 CARD_EXP_DATE_RX.containsMatchIn(haystack) -> FieldKind.CARD_EXP_DATE135 CARD_CVC_RX.containsMatchIn(haystack) -> FieldKind.CARD_CVC136 CARD_NAME_RX.containsMatchIn(haystack) -> FieldKind.CARD_NAME137 else -> FieldKind.NONE138 }139 }140141 private val CARD_NUMBER_RX = Regex("\\b(card\\s*number|credit\\s*card|card\\s*no|pan|cc\\s*num)\\b")142 private val CARD_EXP_MONTH_RX = Regex("\\b(exp\\s*month|expiration\\s*month|expiry\\s*month)\\b")143 private val CARD_EXP_YEAR_RX = Regex("\\b(exp\\s*year|expiration\\s*year|expiry\\s*year)\\b")144 private val CARD_EXP_DATE_RX = Regex("\\b(exp\\s*date|expiration\\s*date|expiry\\s*date|exp\\b)")145 private val CARD_CVC_RX = Regex("\\b(cvc|cvv|security\\s*code|card\\s*code)\\b")146 private val CARD_NAME_RX = Regex("\\b(cardholder|card\\s*holder|name\\s*on\\s*card|card\\s*name)\\b")147}148149150151152153154internal fun authenticatedPackageName(packageName: String?): String? =155 packageName?.takeIf { it.isNotBlank() }