mirror of
https://github.com/aliasvault/aliasvault.git
synced 2026-03-27 02:52:04 -04:00
Improve Android autofill matching to prevent android packages resulting in false positives (#1332)
This commit is contained in:
@@ -194,6 +194,11 @@ class AutofillService : AutofillService() {
|
||||
createCredentialDataset(fieldFinder, credential),
|
||||
)
|
||||
}
|
||||
|
||||
// Add "Open app" option at the bottom (when search text is not shown and there are matches)
|
||||
if (!showSearchText) {
|
||||
responseBuilder.addDataset(createOpenAppDataset(fieldFinder))
|
||||
}
|
||||
}
|
||||
|
||||
callback(responseBuilder.build())
|
||||
@@ -546,4 +551,47 @@ class AutofillService : AutofillService() {
|
||||
|
||||
return dataSetBuilder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a dataset for the "open app" option.
|
||||
* @param fieldFinder The field finder
|
||||
* @return The dataset
|
||||
*/
|
||||
private fun createOpenAppDataset(fieldFinder: FieldFinder): Dataset {
|
||||
// Create presentation for the "open app" option with AliasVault logo
|
||||
val presentation = RemoteViews(packageName, R.layout.autofill_dataset_item_logo)
|
||||
presentation.setTextViewText(
|
||||
R.id.text,
|
||||
getString(R.string.autofill_open_app),
|
||||
)
|
||||
|
||||
val dataSetBuilder = Dataset.Builder(presentation)
|
||||
|
||||
// Create deep link URL to open the credentials page
|
||||
val appInfo = fieldFinder.getAppInfo()
|
||||
val encodedUrl = appInfo?.let { java.net.URLEncoder.encode(it, "UTF-8") } ?: ""
|
||||
val deepLinkUrl = "net.aliasvault.app://credentials?serviceUrl=$encodedUrl"
|
||||
|
||||
// Add a click listener to open AliasVault app with deep link
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = android.net.Uri.parse(deepLinkUrl)
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
val pendingIntent = PendingIntent.getActivity(
|
||||
this@AutofillService,
|
||||
0,
|
||||
intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
||||
)
|
||||
dataSetBuilder.setAuthentication(pendingIntent.intentSender)
|
||||
|
||||
// Add placeholder values to satisfy Android's requirement that at least one value must be set
|
||||
if (fieldFinder.autofillableFields.isNotEmpty()) {
|
||||
for (field in fieldFinder.autofillableFields) {
|
||||
dataSetBuilder.setValue(field.first, AutofillValue.forText(""))
|
||||
}
|
||||
}
|
||||
|
||||
return dataSetBuilder.build()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,42 @@ import net.aliasvault.app.vaultstore.models.Credential
|
||||
*/
|
||||
object CredentialMatcher {
|
||||
|
||||
/**
|
||||
* Common top-level domains (TLDs) that should be excluded from matching.
|
||||
* This prevents false matches when dealing with reversed domain names (Android package names).
|
||||
*/
|
||||
private val commonTlds = setOf(
|
||||
// Generic TLDs
|
||||
"com", "net", "org", "edu", "gov", "mil", "int",
|
||||
// Country code TLDs
|
||||
"nl", "de", "uk", "fr", "it", "es", "pl", "be", "ch", "at", "se", "no", "dk", "fi",
|
||||
"pt", "gr", "cz", "hu", "ro", "bg", "hr", "sk", "si", "lt", "lv", "ee", "ie", "lu",
|
||||
"us", "ca", "mx", "br", "ar", "cl", "co", "ve", "pe", "ec",
|
||||
"au", "nz", "jp", "cn", "in", "kr", "tw", "hk", "sg", "my", "th", "id", "ph", "vn",
|
||||
"za", "eg", "ng", "ke", "ug", "tz", "ma",
|
||||
"ru", "ua", "by", "kz", "il", "tr", "sa", "ae", "qa", "kw",
|
||||
// New gTLDs (common ones)
|
||||
"app", "dev", "io", "ai", "tech", "shop", "store", "online", "site", "website",
|
||||
"blog", "news", "media", "tv", "video", "music", "pro", "info", "biz", "name",
|
||||
)
|
||||
|
||||
/**
|
||||
* Check if a string is likely an Android package name (reversed domain).
|
||||
* Android package names start with TLD followed by dot (e.g., "com.example", "nl.app").
|
||||
* @param text The text to check
|
||||
* @return True if it looks like an Android package name
|
||||
*/
|
||||
private fun isAndroidPackageName(text: String): Boolean {
|
||||
if (!text.contains(".")) {
|
||||
return false
|
||||
}
|
||||
|
||||
val firstPart = text.substringBefore(".").lowercase()
|
||||
|
||||
// Check if first part is a common TLD - this indicates reversed domain (package name)
|
||||
return commonTlds.contains(firstPart)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract domain from URL, handling both full URLs and partial domains.
|
||||
* @param urlString URL or domain string
|
||||
@@ -23,6 +59,12 @@ object CredentialMatcher {
|
||||
// Check if it starts with a protocol
|
||||
val hasProtocol = domain.startsWith("http://") || domain.startsWith("https://")
|
||||
|
||||
// If no protocol and starts with TLD + dot, it's likely an Android package name
|
||||
// Return it as-is for package name matching logic
|
||||
if (!hasProtocol && isAndroidPackageName(domain)) {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Remove protocol if present
|
||||
if (hasProtocol) {
|
||||
domain = domain.replace("https://", "").replace("http://", "")
|
||||
@@ -170,8 +212,8 @@ object CredentialMatcher {
|
||||
}
|
||||
|
||||
return text.lowercase()
|
||||
// Replace common separators and punctuation with spaces
|
||||
.replace(Regex("[|,;:\\-–—/\\\\()\\[\\]{}'\" ~!@#$%^&*+=<>?]"), " ")
|
||||
// Replace common separators and punctuation with spaces (including dots)
|
||||
.replace(Regex("[|,;:\\-–—/\\\\()\\[\\]{}'\" ~!@#$%^&*+=<>?.]"), " ")
|
||||
.split(Regex("\\s+"))
|
||||
.filter { word ->
|
||||
word.length > 3 // Filter out short words
|
||||
|
||||
@@ -295,7 +295,62 @@ class AutofillTest {
|
||||
assertEquals("Reddit", matches[0].service.name)
|
||||
}
|
||||
|
||||
// [#20] - Test multi-part TLDs like .com.au don't match incorrectly
|
||||
// [#20] - Test reversed domain (Android package name) doesn't match on TLD
|
||||
@Test
|
||||
fun testReversedDomainTldCheck() {
|
||||
// Test that dumpert.nl credential doesn't match nl.marktplaats.android package
|
||||
// They both contain "nl" in the name but shouldn't match since "nl" is just a TLD
|
||||
val reversedDomainCredentials = listOf(
|
||||
createTestCredential("Dumpert.nl", "", "user@dumpert.nl"),
|
||||
createTestCredential("Marktplaats.nl", "", "user@marktplaats.nl"),
|
||||
)
|
||||
|
||||
val matches = CredentialMatcher.filterCredentialsByAppInfo(
|
||||
reversedDomainCredentials,
|
||||
"nl.marktplaats.android",
|
||||
)
|
||||
|
||||
// Should only match Marktplaats, not Dumpert (even though both have "nl")
|
||||
assertEquals(1, matches.size)
|
||||
assertEquals("Marktplaats.nl", matches[0].service.name)
|
||||
}
|
||||
|
||||
// [#21] - Test Android package names are properly detected and handled
|
||||
@Test
|
||||
fun testAndroidPackageNameDetection() {
|
||||
val packageCredentials = listOf(
|
||||
createTestCredential("Google App", "com.google.android.googlequicksearchbox", "user@google.com"),
|
||||
createTestCredential("Facebook", "com.facebook.katana", "user@facebook.com"),
|
||||
createTestCredential("WhatsApp", "com.whatsapp", "user@whatsapp.com"),
|
||||
createTestCredential("Generic Site", "example.com", "user@example.com"),
|
||||
)
|
||||
|
||||
// Test com.google.android package matches
|
||||
val googleMatches = CredentialMatcher.filterCredentialsByAppInfo(
|
||||
packageCredentials,
|
||||
"com.google.android.googlequicksearchbox",
|
||||
)
|
||||
assertEquals(1, googleMatches.size)
|
||||
assertEquals("Google App", googleMatches[0].service.name)
|
||||
|
||||
// Test com.facebook package matches
|
||||
val facebookMatches = CredentialMatcher.filterCredentialsByAppInfo(
|
||||
packageCredentials,
|
||||
"com.facebook.katana",
|
||||
)
|
||||
assertEquals(1, facebookMatches.size)
|
||||
assertEquals("Facebook", facebookMatches[0].service.name)
|
||||
|
||||
// Test that web domain doesn't match package name
|
||||
val webMatches = CredentialMatcher.filterCredentialsByAppInfo(
|
||||
packageCredentials,
|
||||
"https://example.com",
|
||||
)
|
||||
assertEquals(1, webMatches.size)
|
||||
assertEquals("Generic Site", webMatches[0].service.name)
|
||||
}
|
||||
|
||||
// [#22] - Test multi-part TLDs like .com.au don't match incorrectly
|
||||
@Test
|
||||
fun testMultiPartTldNoFalseMatches() {
|
||||
// Create test data with different .com.au domains
|
||||
|
||||
Reference in New Issue
Block a user