Match any path component in StrictDomain FilterMode

This commit is contained in:
Fabian Henneke 2020-04-15 17:12:28 +02:00 committed by Harsh Shandilya
parent b633cc1f3d
commit d6db10e089
5 changed files with 86 additions and 16 deletions

View file

@ -121,6 +121,7 @@ dependencies {
// Testing-only dependencies // Testing-only dependencies
androidTestImplementation deps.testing.junit androidTestImplementation deps.testing.junit
androidTestImplementation deps.testing.kotlin_test_junit
androidTestImplementation deps.testing.androidx.runner androidTestImplementation deps.testing.androidx.runner
androidTestImplementation deps.testing.androidx.rules androidTestImplementation deps.testing.androidx.rules
androidTestImplementation deps.testing.androidx.junit androidTestImplementation deps.testing.androidx.junit

View file

@ -0,0 +1,48 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
package com.zeapo.pwdstore
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue
import org.junit.Test as test
private infix fun String.matchedForDomain(domain: String) =
SearchableRepositoryViewModel.generateStrictDomainRegex(domain)?.containsMatchIn(this) == true
class StrictDomainRegexTest {
@test fun acceptsLiteralDomain() {
assertTrue("work/example.org/john.doe@example.org.gpg" matchedForDomain "example.org")
assertTrue("example.org/john.doe@example.org.gpg" matchedForDomain "example.org")
assertTrue("example.org.gpg" matchedForDomain "example.org")
}
@test fun acceptsSubdomains() {
assertTrue("work/www.example.org/john.doe@example.org.gpg" matchedForDomain "example.org")
assertTrue("www2.example.org/john.doe@example.org.gpg" matchedForDomain "example.org")
assertTrue("www.login.example.org.gpg" matchedForDomain "example.org")
}
@test fun rejectsPhishingAttempts() {
assertFalse("example.org.gpg" matchedForDomain "xample.org")
assertFalse("login.example.org.gpg" matchedForDomain "xample.org")
assertFalse("example.org/john.doe@exmple.org.gpg" matchedForDomain "xample.org")
assertFalse("example.org.gpg" matchedForDomain "e/xample.org")
}
@test fun rejectNonGpgComponentMatches() {
assertFalse("work/example.org" matchedForDomain "example.org")
}
@test fun rejectsEmailAddresses() {
assertFalse("work/notexample.org/john.doe@example.org.gpg" matchedForDomain "example.org")
assertFalse("work/notexample.org/john.doe@www.example.org.gpg" matchedForDomain "example.org")
assertFalse("work/john.doe@www.example.org/foo.org" matchedForDomain "example.org")
}
@test fun rejectsPathSeparators() {
assertNull(SearchableRepositoryViewModel.generateStrictDomainRegex("ex/ample.org"))
}
}

View file

@ -126,6 +126,28 @@ enum class ListMode {
@FlowPreview @FlowPreview
class SearchableRepositoryViewModel(application: Application) : AndroidViewModel(application) { class SearchableRepositoryViewModel(application: Application) : AndroidViewModel(application) {
companion object {
fun generateStrictDomainRegex(domain: String): Regex? {
// Valid domains do not contain path separators.
if (domain.contains('/'))
return null
// Matches the start of a path component, which is either the start of the
// string or a path separator.
val prefix = """(?:^|/)"""
val escapedFilter = Regex.escape(domain.replace("/", ""))
// Matches either the filter literally or a strict subdomain of the filter term.
// We allow a lot of freedom in what a subdomain is, as long as it is not an
// email address.
val subdomain = """(?:(?:[^/@]+\.)?$escapedFilter)"""
// Matches the end of a path component, which is either the literal ".gpg" or a
// path separator.
val suffix = """(?:\.gpg|/)"""
// Match any relative path with a component that is a subdomain of the filter.
return Regex(prefix + subdomain + suffix)
}
}
private var _updateCounter = 0 private var _updateCounter = 0
private val updateCounter: Int private val updateCounter: Int
get() = _updateCounter get() = _updateCounter
@ -219,22 +241,18 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
} }
FilterMode.StrictDomain -> { FilterMode.StrictDomain -> {
check(searchAction.listMode == ListMode.FilesOnly) { "Searches with StrictDomain search mode can only list files" } check(searchAction.listMode == ListMode.FilesOnly) { "Searches with StrictDomain search mode can only list files" }
prefilteredResultFlow val regex = generateStrictDomainRegex(searchAction.filter)
.filter { absoluteFile -> if (regex != null) {
val file = absoluteFile.relativeTo(root) prefilteredResultFlow
val toMatch = .filter { absoluteFile ->
directoryStructure.getIdentifierFor(file) ?: return@filter false regex.containsMatchIn(absoluteFile.relativeTo(root).path)
// In strict domain mode, we match }
// * the search term exactly, .map { it.toPasswordItem(root) }
// * subdomains of the search term, .toList()
// * or the search term plus an arbitrary protocol. .sortedWith(itemComparator)
toMatch == searchAction.filter || } else {
toMatch.endsWith(".${searchAction.filter}") || emptyList()
toMatch.endsWith("://${searchAction.filter}") }
}
.map { it.toPasswordItem(root) }
.toList()
.sortedWith(itemComparator)
} }
FilterMode.Fuzzy -> { FilterMode.Fuzzy -> {
prefilteredResultFlow prefilteredResultFlow

View file

@ -50,6 +50,8 @@ subprojects {
targetSdkVersion versions.targetSdk targetSdkVersion versions.targetSdk
versionCode versions.versionCode versionCode versions.versionCode
versionName versions.versionName versionName versions.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8

View file

@ -67,6 +67,7 @@ ext.deps = [
testing: [ testing: [
junit: 'junit:junit:4.13', junit: 'junit:junit:4.13',
kotlin_test_junit: 'org.jetbrains.kotlin:kotlin-test-junit:1.3.71',
androidx: [ androidx: [
runner: 'androidx.test:runner:1.3.0-alpha05', runner: 'androidx.test:runner:1.3.0-alpha05',
rules: 'androidx.test:rules:1.3.0-alpha05', rules: 'androidx.test:rules:1.3.0-alpha05',