Skip to content
This repository has been archived by the owner on Oct 15, 2024. It is now read-only.

Improve and refactor Autofill heuristics #905

Merged
merged 2 commits into from
Jul 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,23 @@ val autofillStrategy = strategy {
// TODO: Introduce a custom fill/generate/update flow for this scenario
rule {
newPassword {
takePair { all { hasAutocompleteHintNewPassword } }
takePair { all { hasHintNewPassword } }
breakTieOnPair { any { isFocused } }
}
currentPassword(optional = true) {
takeSingle { alreadyMatched ->
val adjacentToNewPasswords =
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) {
takeSingle { hasAutocompleteHintUsername }
takeSingle { hasHintUsername }
breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) }
breakTieOnSingle { isFocused }
}
Expand Down Expand Up @@ -73,7 +78,7 @@ val autofillStrategy = strategy {
breakTieOnSingle { isFocused }
}
username(optional = true) {
takeSingle { hasAutocompleteHintUsername }
takeSingle { hasHintUsername }
breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) }
breakTieOnSingle { isFocused }
}
Expand Down Expand Up @@ -115,7 +120,7 @@ val autofillStrategy = strategy {
// field.
rule(applyInSingleOriginMode = true) {
newPassword {
takeSingle { hasAutocompleteHintNewPassword && isFocused }
takeSingle { hasHintNewPassword && isFocused }
}
username(optional = true) {
takeSingle { alreadyMatched ->
Expand Down Expand Up @@ -157,7 +162,7 @@ val autofillStrategy = strategy {
// filling of hidden password fields to scenarios where this is clearly warranted.
rule {
username {
takeSingle { hasAutocompleteHintUsername && isFocused }
takeSingle { hasHintUsername && isFocused }
}
currentPassword(matchHidden = true) {
takeSingle { alreadyMatched ->
Expand All @@ -178,7 +183,7 @@ val autofillStrategy = strategy {
username {
takeSingle { usernameCertainty >= Likely && isFocused }
breakTieOnSingle { usernameCertainty >= Certain }
breakTieOnSingle { hasAutocompleteHintUsername }
breakTieOnSingle { hasHintUsername }
}
}

Expand Down
79 changes: 49 additions & 30 deletions app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,24 @@ class FormField(

companion object {

@RequiresApi(Build.VERSION_CODES.O)
private val HINTS_USERNAME = listOf(HintConstants.AUTOFILL_HINT_USERNAME)
private val HINTS_USERNAME = listOf(
HintConstants.AUTOFILL_HINT_USERNAME,
HintConstants.AUTOFILL_HINT_NEW_USERNAME
)

@RequiresApi(Build.VERSION_CODES.O)
private val HINTS_PASSWORD = listOf(HintConstants.AUTOFILL_HINT_PASSWORD)
private val HINTS_NEW_PASSWORD = listOf(
HintConstants.AUTOFILL_HINT_NEW_PASSWORD
)

@RequiresApi(Build.VERSION_CODES.O)
private val HINTS_OTP = listOf(HintConstants.AUTOFILL_HINT_SMS_OTP)
private val HINTS_PASSWORD = HINTS_NEW_PASSWORD + listOf(
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(
HintConstants.AUTOFILL_HINT_EMAIL_ADDRESS,
HintConstants.AUTOFILL_HINT_NAME,
Expand Down Expand Up @@ -86,7 +94,9 @@ class FormField(
"url_bar", // Chrome/Edge/Firefox address bar
"url_field", // Opera 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(
"pass", "pswd", "pwd"
Expand All @@ -95,10 +105,18 @@ class FormField(
"alias", "e-mail", "email", "login", "user"
)
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!!

// Information for heuristics and exclusion rules based only on the current field
Expand Down Expand Up @@ -151,7 +169,8 @@ class FormField(
private val autofillHints = node.autofillHints?.filter { isSupportedHint(it) } ?: emptyList()
private val excludedByAutofillHints =
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 hasAutofillHintOtp = autofillHints.intersect(HINTS_OTP).isNotEmpty()

Expand All @@ -160,12 +179,18 @@ class FormField(

// Ignored for now, see excludedByHints
private val excludedByAutocompleteHint = htmlAutocomplete == "off"
val hasAutocompleteHintUsername = htmlAutocomplete == "username"
private val hasAutocompleteHintUsername = htmlAutocomplete == "username"
val hasAutocompleteHintCurrentPassword = htmlAutocomplete == "current-password"
val hasAutocompleteHintNewPassword = htmlAutocomplete == "new-password"
private val hasAutocompleteHintNewPassword = htmlAutocomplete == "new-password"
private val hasAutocompleteHintPassword =
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
private val hasAutofillTypeText = node.autofillType == View.AUTOFILL_TYPE_TEXT
Expand All @@ -191,40 +216,34 @@ class FormField(

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,
// 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

// Password field heuristics (based only on the current field)
private val isPossiblePasswordField =
notExcluded && (isAndroidPasswordField || isHtmlPasswordField)
private val isCertainPasswordField =
isPossiblePasswordField && (isHtmlPasswordField || hasAutofillHintPassword || hasAutocompleteHintPassword)
private val isLikelyPasswordField = isPossiblePasswordField && (isCertainPasswordField || (PASSWORD_HEURISTIC_TERMS.any {
fieldId.contains(it) || hint.contains(it) || htmlName.contains(it)
}))
private val isCertainPasswordField = isPossiblePasswordField && hasHintPassword
private val isLikelyPasswordField = isPossiblePasswordField &&
(isCertainPasswordField || PASSWORD_HEURISTIC_TERMS.anyMatchesFieldInfo)
val passwordCertainty =
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)
private val isPossibleOtpField = notExcluded && !isPossiblePasswordField && isTextField
private val isCertainOtpField =
isPossibleOtpField && (hasAutofillHintOtp || hasAutocompleteHintOtp || htmlMaxLength in 6..8)
private val isLikelyOtpField = isPossibleOtpField && (isCertainOtpField || OTP_HEURISTIC_TERMS.any {
fieldId.contains(it) || hint.contains(it) || htmlName.contains(it)
})
private val isCertainOtpField = isPossibleOtpField && hasHintOtp
private val isLikelyOtpField = isPossibleOtpField && (
isCertainOtpField || OTP_HEURISTIC_TERMS.anyMatchesFieldInfo ||
((htmlMaxLength == null || htmlMaxLength in 6..8) && OTP_WEAK_HEURISTIC_TERMS.anyMatchesFieldInfo))
val otpCertainty =
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)
private val isPossibleUsernameField = notExcluded && !isPossiblePasswordField && !isCertainOtpField && isTextField
private val isCertainUsernameField =
isPossibleUsernameField && (hasAutofillHintUsername || hasAutocompleteHintUsername)
private val isLikelyUsernameField = isPossibleUsernameField && (isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.any {
fieldId.contains(it) || hint.contains(it) || htmlName.contains(it)
}))
private val isCertainUsernameField = isPossibleUsernameField && hasHintUsername
private val isLikelyUsernameField = isPossibleUsernameField && (isCertainUsernameField || (USERNAME_HEURISTIC_TERMS.anyMatchesFieldInfo))
val usernameCertainty =
if (isCertainUsernameField) CertaintyLevel.Certain else if (isLikelyUsernameField) CertaintyLevel.Likely else if (isPossibleUsernameField) CertaintyLevel.Possible else CertaintyLevel.Impossible

Expand Down