aboutsummaryrefslogtreecommitdiff
path: root/app/src/main
diff options
context:
space:
mode:
authorFabian Henneke <FabianHenneke@users.noreply.github.com>2020-07-01 09:22:41 +0200
committerGitHub <noreply@github.com>2020-07-01 09:22:41 +0200
commiteaaa3eeea8d437abec9a70135080c2f5e03b205d (patch)
treeef6509c8a360d7eccb04ca32762960097642e082 /app/src/main
parent82a9a6125473dbffa6a76f33c6ff83b4a1bd69b3 (diff)
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.
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt19
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt79
2 files changed, 61 insertions, 37 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt
index 790a72e1..90bb7051 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt
@@ -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 }
}
@@ -73,7 +78,7 @@ val autofillStrategy = strategy {
breakTieOnSingle { isFocused }
}
username(optional = true) {
- takeSingle { hasAutocompleteHintUsername }
+ takeSingle { hasHintUsername }
breakTieOnSingle { alreadyMatched -> directlyPrecedes(alreadyMatched) }
breakTieOnSingle { isFocused }
}
@@ -115,7 +120,7 @@ val autofillStrategy = strategy {
// field.
rule(applyInSingleOriginMode = true) {
newPassword {
- takeSingle { hasAutocompleteHintNewPassword && isFocused }
+ takeSingle { hasHintNewPassword && isFocused }
}
username(optional = true) {
takeSingle { alreadyMatched ->
@@ -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 ->
@@ -178,7 +183,7 @@ val autofillStrategy = strategy {
username {
takeSingle { usernameCertainty >= Likely && isFocused }
breakTieOnSingle { usernameCertainty >= Certain }
- breakTieOnSingle { hasAutocompleteHintUsername }
+ breakTieOnSingle { hasHintUsername }
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt
index 3c1e3a0a..2b18bbb6 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt
@@ -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,
@@ -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"
@@ -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
@@ -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()
@@ -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
@@ -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