Improve and refactor Autofill heuristics (#905)
* Add support for `AUTOFILL_HINT_NEW_PASSWORD` and `AUTOFILL_HINT_NEW_USERNAME`. This allows apps to trigger a `ClassifiedScenario` with only a generate password action and is the analogue of the W3C new-password hint for websites. * Do not consider HTML password fields without hints to be certain password fields (they could contain e.g. bank account numbers, API secrets,...). * Reduce OTP field false positives by excluding the term "postal" as well as fields that match the "code" heuristic term but have HTML maxLength less than 6 or larger than 8. * Add German heuristic term "einmal" ("one-time") for OTP fields * Also exclude fields based on their HTML name (e.g. for terms such as "search"). * Extract fieldId, hint and htmlName matches into an extension property. * Reduce warnings and remove unnecessary suppression annotations.
This commit is contained in:
parent
82a9a61254
commit
eaaa3eeea8
2 changed files with 61 additions and 37 deletions
|
@ -30,18 +30,23 @@ val autofillStrategy = strategy {
|
||||||
// TODO: Introduce a custom fill/generate/update flow for this scenario
|
// TODO: Introduce a custom fill/generate/update flow for this scenario
|
||||||
rule {
|
rule {
|
||||||
newPassword {
|
newPassword {
|
||||||
takePair { all { hasAutocompleteHintNewPassword } }
|
takePair { all { hasHintNewPassword } }
|
||||||
breakTieOnPair { any { isFocused } }
|
breakTieOnPair { any { isFocused } }
|
||||||
}
|
}
|
||||||
currentPassword(optional = true) {
|
currentPassword(optional = true) {
|
||||||
takeSingle { alreadyMatched ->
|
takeSingle { alreadyMatched ->
|
||||||
val adjacentToNewPasswords =
|
val adjacentToNewPasswords =
|
||||||
directlyPrecedes(alreadyMatched) || directlyFollows(alreadyMatched)
|
directlyPrecedes(alreadyMatched) || directlyFollows(alreadyMatched)
|
||||||
hasAutocompleteHintCurrentPassword && adjacentToNewPasswords
|
// The Autofill framework has not hint that applies to current passwords only.
|
||||||
|
// In this scenario, we have already matched fields a pair of fields with a specific
|
||||||
|
// new password hint, so we take a generic Autofill password hint to mean a current
|
||||||
|
// password.
|
||||||
|
(hasAutocompleteHintCurrentPassword || hasAutofillHintPassword) &&
|
||||||
|
adjacentToNewPasswords
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
username(optional = true) {
|
username(optional = true) {
|
||||||
takeSingle { hasAutocompleteHintUsername }
|
takeSingle { hasHintUsername }
|
||||||
breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) }
|
breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) }
|
||||||
breakTieOnSingle { isFocused }
|
breakTieOnSingle { isFocused }
|
||||||
}
|
}
|
||||||
|
@ -73,7 +78,7 @@ val autofillStrategy = strategy {
|
||||||
breakTieOnSingle { isFocused }
|
breakTieOnSingle { isFocused }
|
||||||
}
|
}
|
||||||
username(optional = true) {
|
username(optional = true) {
|
||||||
takeSingle { hasAutocompleteHintUsername }
|
takeSingle { hasHintUsername }
|
||||||
breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) }
|
breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) }
|
||||||
breakTieOnSingle { isFocused }
|
breakTieOnSingle { isFocused }
|
||||||
}
|
}
|
||||||
|
@ -115,7 +120,7 @@ val autofillStrategy = strategy {
|
||||||
// field.
|
// field.
|
||||||
rule(applyInSingleOriginMode = true) {
|
rule(applyInSingleOriginMode = true) {
|
||||||
newPassword {
|
newPassword {
|
||||||
takeSingle { hasAutocompleteHintNewPassword && isFocused }
|
takeSingle { hasHintNewPassword && isFocused }
|
||||||
}
|
}
|
||||||
username(optional = true) {
|
username(optional = true) {
|
||||||
takeSingle { alreadyMatched ->
|
takeSingle { alreadyMatched ->
|
||||||
|
@ -157,7 +162,7 @@ val autofillStrategy = strategy {
|
||||||
// filling of hidden password fields to scenarios where this is clearly warranted.
|
// filling of hidden password fields to scenarios where this is clearly warranted.
|
||||||
rule {
|
rule {
|
||||||
username {
|
username {
|
||||||
takeSingle { hasAutocompleteHintUsername && isFocused }
|
takeSingle { hasHintUsername && isFocused }
|
||||||
}
|
}
|
||||||
currentPassword(matchHidden = true) {
|
currentPassword(matchHidden = true) {
|
||||||
takeSingle { alreadyMatched ->
|
takeSingle { alreadyMatched ->
|
||||||
|
@ -178,7 +183,7 @@ val autofillStrategy = strategy {
|
||||||
username {
|
username {
|
||||||
takeSingle { usernameCertainty >= Likely && isFocused }
|
takeSingle { usernameCertainty >= Likely && isFocused }
|
||||||
breakTieOnSingle { usernameCertainty >= Certain }
|
breakTieOnSingle { usernameCertainty >= Certain }
|
||||||
breakTieOnSingle { hasAutocompleteHintUsername }
|
breakTieOnSingle { hasHintUsername }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,16 +31,24 @@ class FormField(
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
private val HINTS_USERNAME = listOf(
|
||||||
private val HINTS_USERNAME = listOf(HintConstants.AUTOFILL_HINT_USERNAME)
|
HintConstants.AUTOFILL_HINT_USERNAME,
|
||||||
|
HintConstants.AUTOFILL_HINT_NEW_USERNAME
|
||||||
|
)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
private val HINTS_NEW_PASSWORD = listOf(
|
||||||
private val HINTS_PASSWORD = listOf(HintConstants.AUTOFILL_HINT_PASSWORD)
|
HintConstants.AUTOFILL_HINT_NEW_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
private val HINTS_PASSWORD = HINTS_NEW_PASSWORD + listOf(
|
||||||
private val HINTS_OTP = listOf(HintConstants.AUTOFILL_HINT_SMS_OTP)
|
HintConstants.AUTOFILL_HINT_PASSWORD
|
||||||
|
)
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
private val HINTS_OTP = listOf(
|
||||||
|
HintConstants.AUTOFILL_HINT_SMS_OTP
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
private val HINTS_FILLABLE = HINTS_USERNAME + HINTS_PASSWORD + HINTS_OTP + listOf(
|
private val HINTS_FILLABLE = HINTS_USERNAME + HINTS_PASSWORD + HINTS_OTP + listOf(
|
||||||
HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS,
|
HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS,
|
||||||
HintConstants.AUTOFILL_HINT_NAME,
|
HintConstants.AUTOFILL_HINT_NAME,
|
||||||
|
@ -86,7 +94,9 @@ class FormField(
|
||||||
"url_bar", // Chrome/Edge/Firefox address bar
|
"url_bar", // Chrome/Edge/Firefox address bar
|
||||||
"url_field", // Opera address bar
|
"url_field", // Opera address bar
|
||||||
"location_bar_edit_text", // Samsung address bar
|
"location_bar_edit_text", // Samsung address bar
|
||||||
"search", "find", "captcha"
|
"search", "find", "captcha",
|
||||||
|
"postal" // Prevent postal code fields from being mistaken for OTP fields
|
||||||
|
|
||||||
)
|
)
|
||||||
private val PASSWORD_HEURISTIC_TERMS = listOf(
|
private val PASSWORD_HEURISTIC_TERMS = listOf(
|
||||||
"pass", "pswd", "pwd"
|
"pass", "pswd", "pwd"
|
||||||
|
@ -95,10 +105,18 @@ class FormField(
|
||||||
"alias", "e-mail", "email", "login", "user"
|
"alias", "e-mail", "email", "login", "user"
|
||||||
)
|
)
|
||||||
private val OTP_HEURISTIC_TERMS = listOf(
|
private val OTP_HEURISTIC_TERMS = listOf(
|
||||||
"code", "otp"
|
"einmal", "otp"
|
||||||
|
)
|
||||||
|
private val OTP_WEAK_HEURISTIC_TERMS = listOf(
|
||||||
|
"code"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val List<String>.anyMatchesFieldInfo
|
||||||
|
get() = any {
|
||||||
|
fieldId.contains(it) || hint.contains(it) || htmlName.contains(it)
|
||||||
|
}
|
||||||
|
|
||||||
val autofillId: AutofillId = node.autofillId!!
|
val autofillId: AutofillId = node.autofillId!!
|
||||||
|
|
||||||
// Information for heuristics and exclusion rules based only on the current field
|
// Information for heuristics and exclusion rules based only on the current field
|
||||||
|
@ -151,7 +169,8 @@ class FormField(
|
||||||
private val autofillHints = node.autofillHints?.filter { isSupportedHint(it) } ?: emptyList()
|
private val autofillHints = node.autofillHints?.filter { isSupportedHint(it) } ?: emptyList()
|
||||||
private val excludedByAutofillHints =
|
private val excludedByAutofillHints =
|
||||||
if (autofillHints.isEmpty()) false else autofillHints.intersect(HINTS_FILLABLE).isEmpty()
|
if (autofillHints.isEmpty()) false else autofillHints.intersect(HINTS_FILLABLE).isEmpty()
|
||||||
private val hasAutofillHintPassword = autofillHints.intersect(HINTS_PASSWORD).isNotEmpty()
|
val hasAutofillHintPassword = autofillHints.intersect(HINTS_PASSWORD).isNotEmpty()
|
||||||
|
private val hasAutofillHintNewPassword = autofillHints.intersect(HINTS_NEW_PASSWORD).isNotEmpty()
|
||||||
private val hasAutofillHintUsername = autofillHints.intersect(HINTS_USERNAME).isNotEmpty()
|
private val hasAutofillHintUsername = autofillHints.intersect(HINTS_USERNAME).isNotEmpty()
|
||||||
private val hasAutofillHintOtp = autofillHints.intersect(HINTS_OTP).isNotEmpty()
|
private val hasAutofillHintOtp = autofillHints.intersect(HINTS_OTP).isNotEmpty()
|
||||||
|
|
||||||
|
@ -160,12 +179,18 @@ class FormField(
|
||||||
|
|
||||||
// Ignored for now, see excludedByHints
|
// Ignored for now, see excludedByHints
|
||||||
private val excludedByAutocompleteHint = htmlAutocomplete == "off"
|
private val excludedByAutocompleteHint = htmlAutocomplete == "off"
|
||||||
val hasAutocompleteHintUsername = htmlAutocomplete == "username"
|
private val hasAutocompleteHintUsername = htmlAutocomplete == "username"
|
||||||
val hasAutocompleteHintCurrentPassword = htmlAutocomplete == "current-password"
|
val hasAutocompleteHintCurrentPassword = htmlAutocomplete == "current-password"
|
||||||
val hasAutocompleteHintNewPassword = htmlAutocomplete == "new-password"
|
private val hasAutocompleteHintNewPassword = htmlAutocomplete == "new-password"
|
||||||
private val hasAutocompleteHintPassword =
|
private val hasAutocompleteHintPassword =
|
||||||
hasAutocompleteHintCurrentPassword || hasAutocompleteHintNewPassword
|
hasAutocompleteHintCurrentPassword || hasAutocompleteHintNewPassword
|
||||||
val hasAutocompleteHintOtp = htmlAutocomplete == "one-time-code"
|
private val hasAutocompleteHintOtp = htmlAutocomplete == "one-time-code"
|
||||||
|
|
||||||
|
// Results of hint-based field type detection
|
||||||
|
val hasHintUsername = hasAutofillHintUsername || hasAutocompleteHintUsername
|
||||||
|
val hasHintPassword = hasAutofillHintPassword || hasAutocompleteHintPassword
|
||||||
|
val hasHintNewPassword = hasAutofillHintNewPassword || hasAutocompleteHintNewPassword
|
||||||
|
val hasHintOtp = hasAutofillHintOtp || hasAutocompleteHintOtp
|
||||||
|
|
||||||
// Basic autofill exclusion checks
|
// Basic autofill exclusion checks
|
||||||
private val hasAutofillTypeText = node.autofillType == View.AUTOFILL_TYPE_TEXT
|
private val hasAutofillTypeText = node.autofillType == View.AUTOFILL_TYPE_TEXT
|
||||||
|
@ -191,40 +216,34 @@ class FormField(
|
||||||
|
|
||||||
val relevantField = isTextField && hasAutofillTypeText && !excludedByHints
|
val relevantField = isTextField && hasAutofillTypeText && !excludedByHints
|
||||||
|
|
||||||
// Exclude fields based on hint and resource ID
|
// Exclude fields based on hint, resource ID or HTML name.
|
||||||
// Note: We still report excluded fields as relevant since they count for adjacency heuristics,
|
// Note: We still report excluded fields as relevant since they count for adjacency heuristics,
|
||||||
// but ensure that they are never detected as password or username fields.
|
// but ensure that they are never detected as password or username fields.
|
||||||
private val hasExcludedTerm = EXCLUDED_TERMS.any { fieldId.contains(it) || hint.contains(it) }
|
private val hasExcludedTerm = EXCLUDED_TERMS.anyMatchesFieldInfo
|
||||||
private val notExcluded = relevantField && !hasExcludedTerm
|
private val notExcluded = relevantField && !hasExcludedTerm
|
||||||
|
|
||||||
// Password field heuristics (based only on the current field)
|
// Password field heuristics (based only on the current field)
|
||||||
private val isPossiblePasswordField =
|
private val isPossiblePasswordField =
|
||||||
notExcluded && (isAndroidPasswordField || isHtmlPasswordField)
|
notExcluded && (isAndroidPasswordField || isHtmlPasswordField)
|
||||||
private val isCertainPasswordField =
|
private val isCertainPasswordField = isPossiblePasswordField && hasHintPassword
|
||||||
isPossiblePasswordField && (isHtmlPasswordField || hasAutofillHintPassword || hasAutocompleteHintPassword)
|
private val isLikelyPasswordField = isPossiblePasswordField &&
|
||||||
private val isLikelyPasswordField = isPossiblePasswordField && (isCertainPasswordField || (PASSWORD_HEURISTIC_TERMS.any {
|
(isCertainPasswordField || PASSWORD_HEURISTIC_TERMS.anyMatchesFieldInfo)
|
||||||
fieldId.contains(it) || hint.contains(it) || htmlName.contains(it)
|
|
||||||
}))
|
|
||||||
val passwordCertainty =
|
val passwordCertainty =
|
||||||
if (isCertainPasswordField) CertaintyLevel.Certain else if (isLikelyPasswordField) CertaintyLevel.Likely else if (isPossiblePasswordField) CertaintyLevel.Possible else CertaintyLevel.Impossible
|
if (isCertainPasswordField) CertaintyLevel.Certain else if (isLikelyPasswordField) CertaintyLevel.Likely else if (isPossiblePasswordField) CertaintyLevel.Possible else CertaintyLevel.Impossible
|
||||||
|
|
||||||
// OTP field heuristics (based only on the current field)
|
// OTP field heuristics (based only on the current field)
|
||||||
private val isPossibleOtpField = notExcluded && !isPossiblePasswordField && isTextField
|
private val isPossibleOtpField = notExcluded && !isPossiblePasswordField && isTextField
|
||||||
private val isCertainOtpField =
|
private val isCertainOtpField = isPossibleOtpField && hasHintOtp
|
||||||
isPossibleOtpField && (hasAutofillHintOtp || hasAutocompleteHintOtp || htmlMaxLength in 6..8)
|
private val isLikelyOtpField = isPossibleOtpField && (
|
||||||
private val isLikelyOtpField = isPossibleOtpField && (isCertainOtpField || OTP_HEURISTIC_TERMS.any {
|
isCertainOtpField || OTP_HEURISTIC_TERMS.anyMatchesFieldInfo ||
|
||||||
fieldId.contains(it) || hint.contains(it) || htmlName.contains(it)
|
((htmlMaxLength == null || htmlMaxLength in 6..8) && OTP_WEAK_HEURISTIC_TERMS.anyMatchesFieldInfo))
|
||||||
})
|
|
||||||
val otpCertainty =
|
val otpCertainty =
|
||||||
if (isCertainOtpField) CertaintyLevel.Certain else if (isLikelyOtpField) CertaintyLevel.Likely else if (isPossibleOtpField) CertaintyLevel.Possible else CertaintyLevel.Impossible
|
if (isCertainOtpField) CertaintyLevel.Certain else if (isLikelyOtpField) CertaintyLevel.Likely else if (isPossibleOtpField) CertaintyLevel.Possible else CertaintyLevel.Impossible
|
||||||
|
|
||||||
// Username field heuristics (based only on the current field)
|
// Username field heuristics (based only on the current field)
|
||||||
private val isPossibleUsernameField = notExcluded && !isPossiblePasswordField && !isCertainOtpField && isTextField
|
private val isPossibleUsernameField = notExcluded && !isPossiblePasswordField && !isCertainOtpField && isTextField
|
||||||
private val isCertainUsernameField =
|
private val isCertainUsernameField = isPossibleUsernameField && hasHintUsername
|
||||||
isPossibleUsernameField && (hasAutofillHintUsername || hasAutocompleteHintUsername)
|
private val isLikelyUsernameField = isPossibleUsernameField && (isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.anyMatchesFieldInfo))
|
||||||
private val isLikelyUsernameField = isPossibleUsernameField && (isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.any {
|
|
||||||
fieldId.contains(it) || hint.contains(it) || htmlName.contains(it)
|
|
||||||
}))
|
|
||||||
val usernameCertainty =
|
val usernameCertainty =
|
||||||
if (isCertainUsernameField) CertaintyLevel.Certain else if (isLikelyUsernameField) CertaintyLevel.Likely else if (isPossibleUsernameField) CertaintyLevel.Possible else CertaintyLevel.Impossible
|
if (isCertainUsernameField) CertaintyLevel.Certain else if (isLikelyUsernameField) CertaintyLevel.Likely else if (isPossibleUsernameField) CertaintyLevel.Possible else CertaintyLevel.Impossible
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue