BIT-621: Add URI matching for autofill (#842)

This commit is contained in:
Lucas Kivi
2024-01-29 15:33:27 -06:00
committed by Álison Fernandes
parent 0e5e6b4444
commit 0c6ea8d18d
19 changed files with 26674 additions and 95 deletions

View File

@@ -15,6 +15,7 @@ import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessor
import com.x8bit.bitwarden.data.autofill.processor.AutofillProcessorImpl
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProvider
import com.x8bit.bitwarden.data.autofill.provider.AutofillCipherProviderImpl
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
@@ -60,10 +61,12 @@ object AutofillModule {
@Provides
fun providesAutofillCipherProvider(
authRepository: AuthRepository,
cipherMatchingManager: CipherMatchingManager,
vaultRepository: VaultRepository,
): AutofillCipherProvider =
AutofillCipherProviderImpl(
authRepository = authRepository,
cipherMatchingManager = cipherMatchingManager,
vaultRepository = vaultRepository,
)

View File

@@ -4,7 +4,7 @@ import com.bitwarden.core.CipherType
import com.bitwarden.core.CipherView
import com.x8bit.bitwarden.data.auth.repository.AuthRepository
import com.x8bit.bitwarden.data.autofill.model.AutofillCipher
import com.x8bit.bitwarden.data.platform.util.takeIfUriMatches
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.util.subtitle
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import kotlinx.coroutines.flow.first
@@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.first
*/
class AutofillCipherProviderImpl(
private val authRepository: AuthRepository,
private val cipherMatchingManager: CipherMatchingManager,
private val vaultRepository: VaultRepository,
) : AutofillCipherProvider {
private val activeUserId: String? get() = authRepository.activeUserId
@@ -35,7 +36,8 @@ class AutofillCipherProviderImpl(
return cipherViews
.mapNotNull { cipherView ->
cipherView
.takeIf { cipherView.type == CipherType.CARD }
// We only care about non-deleted card ciphers.
.takeIf { cipherView.type == CipherType.CARD && cipherView.deletedDate == null }
?.let { nonNullCipherView ->
AutofillCipher.Card(
name = nonNullCipherView.name,
@@ -54,24 +56,23 @@ class AutofillCipherProviderImpl(
uri: String,
): List<AutofillCipher.Login> {
val cipherViews = getUnlockedCiphersOrNull() ?: return emptyList()
// We only care about non-deleted login ciphers.
val loginCiphers = cipherViews
.filter { it.type == CipherType.LOGIN && it.deletedDate == null }
return cipherViews
.mapNotNull { cipherView ->
cipherView
.takeIf { cipherView.type == CipherType.LOGIN }
// TODO: Get global URI matching value from settings repo and
// TODO: perform more complex URI matching here (BIT-1461).
?.takeIfUriMatches(
uri = uri,
)
?.let { nonNullCipherView ->
AutofillCipher.Login(
name = nonNullCipherView.name,
password = nonNullCipherView.login?.password.orEmpty(),
subtitle = nonNullCipherView.subtitle.orEmpty(),
username = nonNullCipherView.login?.username.orEmpty(),
)
}
return cipherMatchingManager
// Filter for ciphers that match the uri in some way.
.filterCiphersForMatches(
ciphers = loginCiphers,
matchUri = uri,
)
.map { cipherView ->
AutofillCipher.Login(
name = cipherView.name,
password = cipherView.login?.password.orEmpty(),
subtitle = cipherView.subtitle.orEmpty(),
username = cipherView.login?.username.orEmpty(),
)
}
}

View File

@@ -0,0 +1,16 @@
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
import com.bitwarden.core.CipherView
/**
* A manager for matching ciphers based on special criteria.
*/
interface CipherMatchingManager {
/**
* Filter [ciphers] for entries that match the [matchUri] in some fashion.
*/
suspend fun filterCiphersForMatches(
ciphers: List<CipherView>,
matchUri: String,
): List<CipherView>
}

View File

@@ -0,0 +1,274 @@
package com.x8bit.bitwarden.data.platform.manager.ciphermatching
import android.content.Context
import com.bitwarden.core.CipherView
import com.bitwarden.core.LoginUriView
import com.bitwarden.core.UriMatchType
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.platform.util.getDomainOrNull
import com.x8bit.bitwarden.data.platform.util.getHostWithPortOrNull
import com.x8bit.bitwarden.data.platform.util.getWebHostFromAndroidUriOrNull
import com.x8bit.bitwarden.data.platform.util.isAndroidApp
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import com.x8bit.bitwarden.data.vault.repository.model.DomainsData
import com.x8bit.bitwarden.ui.platform.feature.settings.autofill.util.toSdkUriMatchType
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.mapNotNull
import kotlin.text.Regex
import kotlin.text.RegexOption
import kotlin.text.isNullOrBlank
import kotlin.text.lowercase
import kotlin.text.matches
import kotlin.text.startsWith
/**
* The default [CipherMatchingManager] implementation. This class is responsible for matching
* ciphers based on special criteria.
*/
class CipherMatchingManagerImpl(
private val context: Context,
private val settingsRepository: SettingsRepository,
private val vaultRepository: VaultRepository,
) : CipherMatchingManager {
override suspend fun filterCiphersForMatches(
ciphers: List<CipherView>,
matchUri: String,
): List<CipherView> {
val equivalentDomainsData = vaultRepository
.domainsStateFlow
.mapNotNull { it.data }
.first()
val isAndroidApp = matchUri.isAndroidApp()
val defaultUriMatchType = settingsRepository.defaultUriMatchType.toSdkUriMatchType()
val domain = matchUri
.getDomainOrNull(context = context)
?.lowercase()
// Retrieve domains that are considered equivalent to the specified matchUri for cipher
// comparison. If a cipher doesn't have a URI matching the matchUri, but matches a domain in
// matchingDomains, it's considered a match.
val matchingDomains = getMatchingDomains(
domainsData = equivalentDomainsData,
isAndroidApp = isAndroidApp,
matchDomain = domain,
matchUri = matchUri,
)
val exactMatchingCiphers = mutableListOf<CipherView>()
val fuzzyMatchingCiphers = mutableListOf<CipherView>()
ciphers
.forEach { cipherView ->
val matchResult = checkForCipherMatch(
cipherView = cipherView,
context = context,
defaultUriMatchType = defaultUriMatchType,
isAndroidApp = isAndroidApp,
matchUri = matchUri,
matchingDomains = matchingDomains,
)
when (matchResult) {
MatchResult.EXACT -> exactMatchingCiphers.add(cipherView)
MatchResult.FUZZY -> fuzzyMatchingCiphers.add(cipherView)
MatchResult.NONE -> Unit
}
}
return exactMatchingCiphers + fuzzyMatchingCiphers
}
}
/**
* Returns a list of domains that match the specified domain. If the domain is contained within
* the [DomainsData], this will return all matching domains. Otherwise, it will return
* [matchDomain] or [matchUri] depending on [isAndroidApp].
*/
private fun getMatchingDomains(
domainsData: DomainsData,
isAndroidApp: Boolean,
matchDomain: String?,
matchUri: String,
): MatchingDomains {
val androidAppWebHost = matchUri.getWebHostFromAndroidUriOrNull()
val equivalentDomainsList = domainsData
.equivalentDomains
.plus(
elements = domainsData
.globalEquivalentDomains
.map { it.domains },
)
val exactMatchDomains = mutableListOf<String>()
val fuzzyMatchDomains = mutableListOf<String>()
equivalentDomainsList
.forEach { equivalentDomains ->
when {
isAndroidApp && equivalentDomains.contains(matchUri) -> {
exactMatchDomains.addAll(equivalentDomains)
}
isAndroidApp && equivalentDomains.contains(androidAppWebHost) -> {
fuzzyMatchDomains.addAll(equivalentDomains)
}
!isAndroidApp && equivalentDomains.contains(matchDomain) -> {
exactMatchDomains.addAll(equivalentDomains)
}
}
}
// If there are no equivalent domains, add a version of the original URI to the list.
when {
exactMatchDomains.isEmpty() && isAndroidApp -> exactMatchDomains.add(matchUri)
exactMatchDomains.isEmpty() && matchDomain != null -> exactMatchDomains.add(matchDomain)
}
return MatchingDomains(
exactMatches = exactMatchDomains,
fuzzyMatches = fuzzyMatchDomains,
)
}
/**
* Check to see if [cipherView] matches [matchUri] in some way. The returned [MatchResult] will
* provide details on the match quality.
*
* @param cipherView The cipher to be judged for a match.
* @param context A context for getting string resources.
* @param defaultUriMatchType The global default [UriMatchType].
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
* @param matchingDomains The set of domains that match the domain of [matchUri].
* @param matchUri The uri that this cipher is being matched to.
*/
@Suppress("LongParameterList")
private fun checkForCipherMatch(
cipherView: CipherView,
context: Context,
defaultUriMatchType: UriMatchType,
isAndroidApp: Boolean,
matchingDomains: MatchingDomains,
matchUri: String,
): MatchResult {
val matchResults = cipherView
.login
?.uris
?.map { loginUriView ->
loginUriView.checkForMatch(
context = context,
defaultUriMatchType = defaultUriMatchType,
isAndroidApp = isAndroidApp,
matchingDomains = matchingDomains,
matchUri = matchUri,
)
}
return matchResults
?.firstOrNull { it == MatchResult.EXACT }
?: matchResults
?.firstOrNull { it == MatchResult.FUZZY }
?: MatchResult.NONE
}
/**
* Check to see how well this [LoginUriView] matches [matchUri].
*
* @param context A context for getting app information.
* @param defaultUriMatchType The global default [UriMatchType].
* @param isAndroidApp Whether or not the [matchUri] belongs to an Android app.
* @param matchingDomains The set of domains that match the domain of [matchUri].
* @param matchUri The uri that this [LoginUriView] is being matched to.
*/
private fun LoginUriView.checkForMatch(
context: Context,
defaultUriMatchType: UriMatchType,
isAndroidApp: Boolean,
matchingDomains: MatchingDomains,
matchUri: String,
): MatchResult {
val matchType = this.match ?: defaultUriMatchType
val loginViewUri = this.uri
return if (!loginViewUri.isNullOrBlank()) {
when (matchType) {
UriMatchType.DOMAIN -> {
checkUriForDomainMatch(
context = context,
isAndroidApp = isAndroidApp,
matchingDomains = matchingDomains,
uri = loginViewUri,
)
}
UriMatchType.EXACT -> exactIfTrue(loginViewUri == matchUri)
UriMatchType.HOST -> {
val loginUriHost = loginViewUri.getHostWithPortOrNull()
val matchUriHost = matchUri.getHostWithPortOrNull()
exactIfTrue(matchUriHost != null && loginUriHost == matchUriHost)
}
UriMatchType.NEVER -> MatchResult.NONE
UriMatchType.REGULAR_EXPRESSION -> {
val pattern = Regex(loginViewUri, RegexOption.IGNORE_CASE)
exactIfTrue(matchUri.matches(pattern))
}
UriMatchType.STARTS_WITH -> exactIfTrue(matchUri.startsWith(loginViewUri))
}
} else {
MatchResult.NONE
}
}
/**
* Check to see if [uri] matches [matchingDomains] in some way.
*/
private fun checkUriForDomainMatch(
isAndroidApp: Boolean,
context: Context,
matchingDomains: MatchingDomains,
uri: String,
): MatchResult = when {
matchingDomains.exactMatches.contains(uri) -> MatchResult.EXACT
isAndroidApp && matchingDomains.fuzzyMatches.contains(uri) -> MatchResult.FUZZY
else -> {
val domain = uri
.getDomainOrNull(context = context)
?.lowercase()
// We only care about fuzzy matches if we are isAndroidApp is true because the fuzzu
// matches are generated using a app URI derived host.
when {
matchingDomains.exactMatches.contains(domain) -> MatchResult.EXACT
isAndroidApp && matchingDomains.fuzzyMatches.contains(domain) -> MatchResult.FUZZY
else -> MatchResult.NONE
}
}
}
/**
* Simple function to return [MatchResult.EXACT] if [condition] is true, and
* [MatchResult.NONE] otherwise.
*/
private fun exactIfTrue(condition: Boolean): MatchResult =
if (condition) MatchResult.EXACT else MatchResult.NONE
/**
* A convenience data class for holding domain matches.
*/
private data class MatchingDomains(
val exactMatches: List<String>,
val fuzzyMatches: List<String>,
)
/**
* A enum to represent the quality of a match.
*/
private enum class MatchResult {
EXACT,
FUZZY,
NONE,
}

View File

@@ -22,11 +22,15 @@ import com.x8bit.bitwarden.data.platform.manager.PushManager
import com.x8bit.bitwarden.data.platform.manager.PushManagerImpl
import com.x8bit.bitwarden.data.platform.manager.SdkClientManager
import com.x8bit.bitwarden.data.platform.manager.SdkClientManagerImpl
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManager
import com.x8bit.bitwarden.data.platform.manager.ciphermatching.CipherMatchingManagerImpl
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManager
import com.x8bit.bitwarden.data.platform.manager.clipboard.BitwardenClipboardManagerImpl
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManager
import com.x8bit.bitwarden.data.platform.manager.dispatcher.DispatcherManagerImpl
import com.x8bit.bitwarden.data.platform.repository.EnvironmentRepository
import com.x8bit.bitwarden.data.platform.repository.SettingsRepository
import com.x8bit.bitwarden.data.vault.repository.VaultRepository
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -48,6 +52,19 @@ object PlatformManagerModule {
fun provideAppForegroundManager(): AppForegroundManager =
AppForegroundManagerImpl()
@Provides
@Singleton
fun providesCipherMatchingManager(
@ApplicationContext context: Context,
settingsRepository: SettingsRepository,
vaultRepository: VaultRepository,
): CipherMatchingManager =
CipherMatchingManagerImpl(
context = context,
settingsRepository = settingsRepository,
vaultRepository = vaultRepository,
)
@Provides
@Singleton
fun provideClock(): Clock = Clock.systemDefaultZone()

View File

@@ -0,0 +1,27 @@
package com.x8bit.bitwarden.data.platform.manager.model
/**
* A data class containing the result of parsing the URL.
*
* Example:
* - URL: m.google.com
* - domain: google.com
* - secondLevelDomain: google
* - subDomain: m
* - topLevelDomain: com
*
* @property secondLevelDomain The second-level domain of the URL, if it exists.
* @property subDomain The subdomain of the URL.
* @property topLevelDomain The top-level domain (TLD) of the URL.
*/
data class DomainName(
val secondLevelDomain: String?,
val subDomain: String?,
val topLevelDomain: String,
) {
/**
* The domain of the URL, constructed from the second-level and top-level domains.
*/
val domain: String
get() = "$secondLevelDomain.$topLevelDomain"
}

View File

@@ -68,18 +68,3 @@ private val CardView.subtitleCardNumber: String?
*/
private val String?.isAmEx: Boolean
get() = this?.startsWith("34") == true || this?.startsWith("37") == true
/**
* Take this [CipherView] if its uri matches [uri]. Otherwise, return null.
*/
fun CipherView.takeIfUriMatches(
uri: String,
): CipherView? =
// TODO: Pass global URI matching value from settings (BIT-1461)
this
.takeIf {
// TODO: perform comprehensive URI matching (BIT-1461)
login
?.uris
?.any { it.uri == uri } == true
}

View File

@@ -0,0 +1,86 @@
package com.x8bit.bitwarden.data.platform.util
import android.content.Context
import com.x8bit.bitwarden.data.platform.annotation.OmitFromCoverage
import java.net.URI
import java.net.URISyntaxException
/**
* The protocol for and Android app URI.
*/
private const val ANDROID_APP_PROTOCOL: String = "androidapp://"
/**
* Try creating a [URI] out of this [String]. If it fails, return null.
*/
fun String.toUriOrNull(): URI? =
try {
URI(this)
} catch (e: URISyntaxException) {
null
}
/**
* Whether this [String] represents an android app URI.
*/
fun String.isAndroidApp(): Boolean =
this.startsWith(ANDROID_APP_PROTOCOL)
/**
* Try and extract the web host from this [String] if it represents an Android app.
*/
fun String.getWebHostFromAndroidUriOrNull(): String? =
if (this.isAndroidApp()) {
val components = this
.replace(ANDROID_APP_PROTOCOL, "")
.split('.')
if (components.size > 1) {
"${components[1]}.${components[0]}"
} else {
null
}
} else {
null
}
/**
* Extract the domain name from this [String] if possible, otherwise return null.
*/
fun String.getDomainOrNull(context: Context): String? =
this
.toUriOrNull()
?.parseDomainOrNull(context = context)
/**
* Extract the host with port from this [String] if possible, otherwise return null.
*/
@OmitFromCoverage
fun String.getHostWithPortOrNull(): String? =
this
.toUriOrNull()
?.let { uri ->
val host = uri.host
val port = uri.port
if (host != null && port != -1) {
"$host:$port"
} else {
null
}
}
/**
* Find the indices of the last occurrences of [substring] within this [String]. Return null if no
* occurrences are found.
*/
fun String.findLastSubstringIndicesOrNull(substring: String): IntRange? {
val lastIndex = this.lastIndexOf(substring)
return if (lastIndex != -1) {
val endIndex = lastIndex + substring.length - 1
IntRange(lastIndex, endIndex)
} else {
null
}
}

View File

@@ -0,0 +1,223 @@
package com.x8bit.bitwarden.data.platform.util
import android.content.Context
import com.x8bit.bitwarden.R
import com.x8bit.bitwarden.data.platform.manager.model.DomainName
import java.net.URI
/**
* A regular expression that matches IP addresses.
*/
private const val IP_REGEX: String =
"^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\." +
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
/**
* Parses the base domain from the URL. Returns null if unavailable.
*/
fun URI.parseDomainOrNull(context: Context): String? {
val host = this?.host ?: return null
val isIpAddress = host.matches(IP_REGEX.toRegex())
return if (host == "localhost" || isIpAddress) {
host
} else {
parseDomainNameOrNullInternal(
context = context,
host = host,
)
?.domain
}
}
/**
* Parses a URL to get the breakdown of a URL's domain. Returns null if invalid.
*/
fun URI.parseDomainNameOrNull(context: Context): DomainName? =
this
// URI is a platform type and host can be null.
?.host
?.let { nonNullHost ->
parseDomainNameOrNullInternal(
context = context,
host = nonNullHost,
)
}
/**
* The internal implementation of [parseDomainNameOrNull]. This doesn't extend URI and has a
* non-null [host] parameter. Technically, URI.host could be null and we want to avoid issues with
* that.
*/
@Suppress("LongMethod")
private fun parseDomainNameOrNullInternal(
context: Context,
host: String,
): DomainName? {
val exceptionSuffixes = context
.resources
.getStringArray(R.array.exception_suffixes)
.toList()
val normalSuffixes = context
.resources
.getStringArray(R.array.normal_suffixes)
.toList()
val wildCardSuffixes = context
.resources
.getStringArray(R.array.wild_card_suffixes)
.toList()
// Split the host into parts separated by a period. Start with the last part and incrementally
// add back the earlier parts to build a list of any matching domains in the data set.
val hostParts = host
.split(".")
.reversed()
var partialDomain = ""
val ruleMatches: MutableList<SuffixMatchType> = mutableListOf()
// Check to see if this part of the host belongs to any of the suffix lists.
hostParts
.forEach { hostPart ->
partialDomain = if (partialDomain.isBlank()) {
hostPart
} else {
"$hostPart.$partialDomain"
}
when {
// Normal suffixes first.
normalSuffixes.contains(partialDomain) -> {
ruleMatches.add(
SuffixMatchType.Normal(
partialDomain = partialDomain,
),
)
}
// Then wild cards.
wildCardSuffixes.contains(partialDomain) -> {
ruleMatches.add(
SuffixMatchType.WildCard(
partialDomain = partialDomain,
),
)
}
// And finally, exceptions.
exceptionSuffixes.contains(partialDomain) -> {
ruleMatches.add(
SuffixMatchType.Exception(
partialDomain = partialDomain,
),
)
}
}
}
// Take only the largest public suffix match that occurs within our URI's host. We want the
// largest because if the URI was "airbnb.co.uk" we our list would contain "uk" and "co.uk",
// which are both valid top level domains. In this case, "uk" is just simply not the top level
// domain.
val largestMatch = ruleMatches.maxByOrNull {
it
.partialDomain
.split('.')
.size
}
// Determine the position of the top level domain within the host.
val tldRange: IntRange? = when (largestMatch) {
is SuffixMatchType.Exception,
is SuffixMatchType.Normal,
-> {
host.findLastSubstringIndicesOrNull(largestMatch.partialDomain)
}
is SuffixMatchType.WildCard -> {
// This gets the last portion of the top level domain.
val nonWildcardTldIndex = host.lastIndexOf(".${largestMatch.partialDomain}")
if (nonWildcardTldIndex != -1) {
val nonWildcardTld = host.substring(0, nonWildcardTldIndex)
// But we need to also match the wildcard portion.
val dotIndex = nonWildcardTld.lastIndexOf(".")
if (dotIndex != -1) {
IntRange(dotIndex + 1, nonWildcardTldIndex - 1)
} else {
null
}
} else {
null
}
}
null -> null
}
return tldRange
?.first
?.let { firstIndex ->
val topLevelDomain = host.substring(firstIndex)
// Parse the remaining parts prior to the TLD.
// - If there's 0 parts left, there is just a TLD and no domain or subdomain.
// - If there's 1 part, it's the domain, and there is no subdomain.
// - If there's 2+ parts, the last part is the domain, the other parts (combined) are
// the subdomain.
val possibleSubDomainAndDomain = if (firstIndex > 0) {
host.substring(0, firstIndex - 1)
} else {
null
}
val subDomainAndDomainParts = possibleSubDomainAndDomain?.split(".")
val secondLevelDomain = subDomainAndDomainParts?.lastOrNull()
val subDomain = subDomainAndDomainParts
?.dropLast(1)
?.joinToString(separator = ".")
// joinToString leaves white space if called on an empty list.
// So only take the string if it wasn't empty after the dropLast(1).
.takeIf { (subDomainAndDomainParts?.size ?: 0) > 1 }
DomainName(
secondLevelDomain = secondLevelDomain,
topLevelDomain = topLevelDomain,
subDomain = subDomain,
)
}
}
/**
* A type of domain suffix match.
*/
private sealed class SuffixMatchType {
/**
* The partial domain that was actually matched.
*/
abstract val partialDomain: String
/**
* The match occurred with an exception suffix that starts with '!'.
*/
data class Exception(
override val partialDomain: String,
) : SuffixMatchType()
/**
* The match occurred with a normal suffix.
*/
data class Normal(
override val partialDomain: String,
) : SuffixMatchType()
/**
* The match occurred with a wildcard suffix that starts with '*'.
*/
data class WildCard(
override val partialDomain: String,
) : SuffixMatchType()
}

View File

@@ -18,3 +18,16 @@ val UriMatchType.displayLabel: Text
UriMatchType.NEVER -> R.string.never
}
.asText()
/**
* Convert this internal [UriMatchType] to the sdk model.
*/
fun UriMatchType.toSdkUriMatchType(): com.bitwarden.core.UriMatchType =
when (this) {
UriMatchType.DOMAIN -> com.bitwarden.core.UriMatchType.DOMAIN
UriMatchType.EXACT -> com.bitwarden.core.UriMatchType.EXACT
UriMatchType.HOST -> com.bitwarden.core.UriMatchType.HOST
UriMatchType.NEVER -> com.bitwarden.core.UriMatchType.NEVER
UriMatchType.REGULAR_EXPRESSION -> com.bitwarden.core.UriMatchType.REGULAR_EXPRESSION
UriMatchType.STARTS_WITH -> com.bitwarden.core.UriMatchType.STARTS_WITH
}