diff --git a/.github/workflows/update_publicsuffix_data.yml b/.github/workflows/update_publicsuffix_data.yml
index 34722279..29f1777f 100644
--- a/.github/workflows/update_publicsuffix_data.yml
+++ b/.github/workflows/update_publicsuffix_data.yml
@@ -16,7 +16,7 @@ jobs:
git config user.email noreply@github.com
- name: Download new publicsuffix data
- run: curl -L https://github.com/mozilla-mobile/android-components/raw/master/components/lib/publicsuffixlist/src/main/assets/publicsuffixes -o app/src/main/assets/publicsuffixes
+ run: curl -L https://github.com/mozilla-mobile/android-components/raw/master/components/lib/publicsuffixlist/src/main/assets/publicsuffixes -o autofill-parser/src/main/assets/publicsuffixes
- name: Compare list changes
run: if [[ $(git diff --binary --stat) != '' ]]; then echo "::set-env name=UPDATED::true"; fi
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index f7e941c7..be83284a 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -12,11 +12,11 @@
+
-
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 2d5c7262..293af246 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -89,7 +89,6 @@ android {
dependencies {
compileOnly(Dependencies.AndroidX.annotation)
implementation(Dependencies.AndroidX.activity_ktx)
- implementation(Dependencies.AndroidX.autofill)
implementation(Dependencies.AndroidX.appcompat)
implementation(Dependencies.AndroidX.biometric)
implementation(Dependencies.AndroidX.constraint_layout)
@@ -109,6 +108,7 @@ dependencies {
implementation(Dependencies.Kotlin.Coroutines.android)
implementation(Dependencies.Kotlin.Coroutines.core)
+ implementation(project(Dependencies.FirstParty.autofill_parser))
implementation(Dependencies.FirstParty.openpgp_ktx)
implementation(Dependencies.FirstParty.zxing_android_embedded)
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
index 4e4bf85e..eb339ac3 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
@@ -35,6 +35,8 @@ import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.i
import com.github.ajalt.timberkt.w
+import com.github.androidpasswordstore.autofillparser.BrowserAutofillSupportLevel
+import com.github.androidpasswordstore.autofillparser.getInstalledBrowsersWithAutofillSupportLevel
import com.github.michaelbull.result.fold
import com.github.michaelbull.result.getOr
import com.github.michaelbull.result.onFailure
@@ -43,8 +45,6 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputEditText
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
-import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel
-import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel
import com.zeapo.pwdstore.crypto.BasePgpActivity.Companion.getLongName
import com.zeapo.pwdstore.crypto.DecryptActivity
import com.zeapo.pwdstore.crypto.PasswordCreationActivity
diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
index d41a7fe7..7aeec148 100644
--- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
@@ -36,13 +36,13 @@ import androidx.preference.SwitchPreferenceCompat
import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.w
+import com.github.androidpasswordstore.autofillparser.BrowserAutofillSupportLevel
+import com.github.androidpasswordstore.autofillparser.getInstalledBrowsersWithAutofillSupportLevel
import com.github.michaelbull.result.getOr
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
-import com.zeapo.pwdstore.autofill.oreo.BrowserAutofillSupportLevel
-import com.zeapo.pwdstore.autofill.oreo.getInstalledBrowsersWithAutofillSupportLevel
import com.zeapo.pwdstore.crypto.BasePgpActivity
import com.zeapo.pwdstore.git.GitConfigActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillMatcher.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillMatcher.kt
index 1b0ff35d..601be34a 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillMatcher.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillMatcher.kt
@@ -14,6 +14,8 @@ import com.github.ajalt.timberkt.w
import com.github.michaelbull.result.Err
import com.github.michaelbull.result.Ok
import com.github.michaelbull.result.Result
+import com.github.androidpasswordstore.autofillparser.FormOrigin
+import com.github.androidpasswordstore.autofillparser.computeCertificatesHash
import com.zeapo.pwdstore.R
import java.io.File
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt
index 9c84e1ad..cc0875f3 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt
@@ -7,6 +7,8 @@ package com.zeapo.pwdstore.autofill.oreo
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
+import com.github.androidpasswordstore.autofillparser.Credentials
+import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import java.nio.file.Paths
@@ -121,4 +123,17 @@ object AutofillPreferences {
val value = context.sharedPrefs.getString(DirectoryStructure.PREFERENCE, null)
return DirectoryStructure.fromValue(value)
}
+
+ fun credentialsFromStoreEntry(
+ context: Context,
+ file: File,
+ entry: PasswordEntry,
+ directoryStructure: DirectoryStructure
+ ): Credentials {
+ // Always give priority to a username stored in the encrypted extras
+ val username = entry.username
+ ?: directoryStructure.getUsernameFor(file)
+ ?: context.getDefaultUsername()
+ return Credentials(username, entry.password, entry.calculateTotpCode())
+ }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt
new file mode 100644
index 00000000..0e73b908
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt
@@ -0,0 +1,192 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.autofill.oreo
+
+import android.content.Context
+import android.content.IntentSender
+import android.os.Build
+import android.os.Bundle
+import android.service.autofill.Dataset
+import android.service.autofill.FillCallback
+import android.service.autofill.FillResponse
+import android.service.autofill.SaveInfo
+import android.widget.RemoteViews
+import androidx.annotation.RequiresApi
+import com.github.ajalt.timberkt.e
+import com.github.michaelbull.result.fold
+import com.github.androidpasswordstore.autofillparser.AutofillAction
+import com.github.androidpasswordstore.autofillparser.AutofillScenario
+import com.github.androidpasswordstore.autofillparser.Credentials
+import com.github.androidpasswordstore.autofillparser.FillableForm
+import com.github.androidpasswordstore.autofillparser.fillWith
+import com.zeapo.pwdstore.autofill.oreo.ui.AutofillDecryptActivity
+import com.zeapo.pwdstore.autofill.oreo.ui.AutofillFilterView
+import com.zeapo.pwdstore.autofill.oreo.ui.AutofillPublisherChangedActivity
+import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity
+import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSmsActivity
+import java.io.File
+
+@RequiresApi(Build.VERSION_CODES.O)
+class AutofillResponseBuilder(form: FillableForm) {
+ private val formOrigin = form.formOrigin
+ private val scenario = form.scenario
+ private val ignoredIds = form.ignoredIds
+ private val saveFlags = form.saveFlags
+ private val clientState = form.toClientState()
+
+ // We do not offer save when the only relevant field is a username field or there is no field.
+ private val scenarioSupportsSave =
+ scenario.fieldsToSave.minus(listOfNotNull(scenario.username)).isNotEmpty()
+ private val canBeSaved = saveFlags != null && scenarioSupportsSave
+
+ private fun makePlaceholderDataset(
+ remoteView: RemoteViews,
+ intentSender: IntentSender,
+ action: AutofillAction
+ ): Dataset {
+ return Dataset.Builder(remoteView).run {
+ fillWith(scenario, action, credentials = null)
+ setAuthentication(intentSender)
+ build()
+ }
+ }
+
+ private fun makeMatchDataset(context: Context, file: File): Dataset? {
+ if (scenario.fieldsToFillOn(AutofillAction.Match).isEmpty()) return null
+ val remoteView = makeFillMatchRemoteView(context, file, formOrigin)
+ val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
+ return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Match)
+ }
+
+ private fun makeSearchDataset(context: Context): Dataset? {
+ if (scenario.fieldsToFillOn(AutofillAction.Search).isEmpty()) return null
+ val remoteView = makeSearchAndFillRemoteView(context, formOrigin)
+ val intentSender =
+ AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin)
+ return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Search)
+ }
+
+ private fun makeGenerateDataset(context: Context): Dataset? {
+ if (scenario.fieldsToFillOn(AutofillAction.Generate).isEmpty()) return null
+ val remoteView = makeGenerateAndFillRemoteView(context, formOrigin)
+ val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin)
+ return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Generate)
+ }
+
+ private fun makeFillOtpFromSmsDataset(context: Context): Dataset? {
+ if (scenario.fieldsToFillOn(AutofillAction.FillOtpFromSms).isEmpty()) return null
+ if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
+ val remoteView = makeFillOtpFromSmsRemoteView(context, formOrigin)
+ val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context)
+ return makePlaceholderDataset(remoteView, intentSender, AutofillAction.FillOtpFromSms)
+ }
+
+ private fun makePublisherChangedDataset(
+ context: Context,
+ publisherChangedException: AutofillPublisherChangedException
+ ): Dataset {
+ val remoteView = makeWarningRemoteView(context)
+ val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender(
+ context, publisherChangedException
+ )
+ return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Match)
+ }
+
+ private fun makePublisherChangedResponse(
+ context: Context,
+ publisherChangedException: AutofillPublisherChangedException
+ ): FillResponse {
+ return FillResponse.Builder().run {
+ addDataset(makePublisherChangedDataset(context, publisherChangedException))
+ setIgnoredIds(*ignoredIds.toTypedArray())
+ build()
+ }
+ }
+
+ // TODO: Support multi-step authentication flows in apps via FLAG_DELAY_SAVE
+ // See: https://developer.android.com/reference/android/service/autofill/SaveInfo#FLAG_DELAY_SAVE
+ private fun makeSaveInfo(): SaveInfo? {
+ if (!canBeSaved) return null
+ check(saveFlags != null)
+ val idsToSave = scenario.fieldsToSave.map { it.autofillId }.toTypedArray()
+ if (idsToSave.isEmpty()) return null
+ var saveDataTypes = SaveInfo.SAVE_DATA_TYPE_PASSWORD
+ if (scenario.username != null) {
+ saveDataTypes = saveDataTypes or SaveInfo.SAVE_DATA_TYPE_USERNAME
+ }
+ return SaveInfo.Builder(saveDataTypes, idsToSave).run {
+ setFlags(saveFlags)
+ build()
+ }
+ }
+
+ private fun makeFillResponse(context: Context, matchedFiles: List): FillResponse? {
+ var hasDataset = false
+ return FillResponse.Builder().run {
+ for (file in matchedFiles) {
+ makeMatchDataset(context, file)?.let {
+ hasDataset = true
+ addDataset(it)
+ }
+ }
+ makeSearchDataset(context)?.let {
+ hasDataset = true
+ addDataset(it)
+ }
+ makeGenerateDataset(context)?.let {
+ hasDataset = true
+ addDataset(it)
+ }
+ makeFillOtpFromSmsDataset(context)?.let {
+ hasDataset = true
+ addDataset(it)
+ }
+ if (!hasDataset) return null
+ makeSaveInfo()?.let { setSaveInfo(it) }
+ setClientState(clientState)
+ setIgnoredIds(*ignoredIds.toTypedArray())
+ build()
+ }
+ }
+
+ /**
+ * Creates and returns a suitable [FillResponse] to the Autofill framework.
+ */
+ fun fillCredentials(context: Context, callback: FillCallback) {
+ AutofillMatcher.getMatchesFor(context, formOrigin).fold(
+ success = { matchedFiles ->
+ callback.onSuccess(makeFillResponse(context, matchedFiles))
+ },
+ failure = { e ->
+ e(e)
+ callback.onSuccess(makePublisherChangedResponse(context, e))
+ }
+ )
+ }
+
+ companion object {
+ fun makeFillInDataset(
+ context: Context,
+ credentials: Credentials,
+ clientState: Bundle,
+ action: AutofillAction
+ ): Dataset {
+ val remoteView = makePlaceholderRemoteView(context)
+ val scenario = AutofillScenario.fromBundle(clientState)
+ // Before Android P, Datasets used for fill-in had to come with a RemoteViews, even
+ // though they are never shown.
+ val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ Dataset.Builder()
+ } else {
+ Dataset.Builder(remoteView)
+ }
+ return builder.run {
+ if (scenario != null) fillWith(scenario, action, credentials)
+ else e { "Failed to recover scenario from client state" }
+ build()
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillViewUtils.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillViewUtils.kt
new file mode 100644
index 00000000..5e8061a2
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillViewUtils.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.autofill.oreo
+
+import android.content.Context
+import android.widget.RemoteViews
+import com.github.androidpasswordstore.autofillparser.FormOrigin
+import com.zeapo.pwdstore.R
+import com.zeapo.pwdstore.utils.PasswordRepository
+import java.io.File
+
+private fun makeRemoteView(
+ context: Context,
+ title: String,
+ summary: String,
+ iconRes: Int
+): RemoteViews {
+ return RemoteViews(context.packageName, R.layout.oreo_autofill_dataset).apply {
+ setTextViewText(R.id.title, title)
+ setTextViewText(R.id.summary, summary)
+ setImageViewResource(R.id.icon, iconRes)
+ }
+}
+
+fun makeFillMatchRemoteView(context: Context, file: File, formOrigin: FormOrigin): RemoteViews {
+ val title = formOrigin.getPrettyIdentifier(context, untrusted = false)
+ val directoryStructure = AutofillPreferences.directoryStructure(context)
+ val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory())
+ val summary = directoryStructure.getUsernameFor(relativeFile)
+ ?: directoryStructure.getPathToIdentifierFor(relativeFile) ?: ""
+ val iconRes = R.drawable.ic_person_black_24dp
+ return makeRemoteView(context, title, summary, iconRes)
+}
+
+fun makeSearchAndFillRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews {
+ val title = formOrigin.getPrettyIdentifier(context, untrusted = true)
+ val summary = context.getString(R.string.oreo_autofill_search_in_store)
+ val iconRes = R.drawable.ic_search_black_24dp
+ return makeRemoteView(context, title, summary, iconRes)
+}
+
+fun makeGenerateAndFillRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews {
+ val title = formOrigin.getPrettyIdentifier(context, untrusted = true)
+ val summary = context.getString(R.string.oreo_autofill_generate_password)
+ val iconRes = R.drawable.ic_autofill_new_password
+ return makeRemoteView(context, title, summary, iconRes)
+}
+
+fun makeFillOtpFromSmsRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews {
+ val title = formOrigin.getPrettyIdentifier(context, untrusted = true)
+ val summary = context.getString(R.string.oreo_autofill_fill_otp_from_sms)
+ val iconRes = R.drawable.ic_autofill_sms
+ return makeRemoteView(context, title, summary, iconRes)
+}
+
+fun makePlaceholderRemoteView(context: Context): RemoteViews {
+ return makeRemoteView(context, "PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher)
+}
+
+fun makeWarningRemoteView(context: Context): RemoteViews {
+ val title = context.getString(R.string.oreo_autofill_warning_publisher_dataset_title)
+ val summary = context.getString(R.string.oreo_autofill_warning_publisher_dataset_summary)
+ val iconRes = R.drawable.ic_warning_red_24dp
+ return makeRemoteView(context, title, summary, iconRes)
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt
deleted file mode 100644
index 5d24c882..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Form.kt
+++ /dev/null
@@ -1,383 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore.autofill.oreo
-
-import android.app.assist.AssistStructure
-import android.content.Context
-import android.content.IntentSender
-import android.content.pm.PackageManager
-import android.net.Uri
-import android.os.Build
-import android.os.Bundle
-import android.service.autofill.Dataset
-import android.service.autofill.FillCallback
-import android.service.autofill.FillResponse
-import android.service.autofill.SaveInfo
-import android.view.autofill.AutofillId
-import android.widget.RemoteViews
-import androidx.annotation.RequiresApi
-import androidx.core.os.bundleOf
-import com.github.ajalt.timberkt.d
-import com.github.ajalt.timberkt.e
-import com.github.michaelbull.result.fold
-import com.zeapo.pwdstore.autofill.oreo.ui.AutofillDecryptActivity
-import com.zeapo.pwdstore.autofill.oreo.ui.AutofillFilterView
-import com.zeapo.pwdstore.autofill.oreo.ui.AutofillPublisherChangedActivity
-import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity
-import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSmsActivity
-import java.io.File
-
-/**
- * A unique identifier for either an Android app (package name) or a website (origin minus port).
- */
-sealed class FormOrigin(open val identifier: String) {
-
- data class Web(override val identifier: String) : FormOrigin(identifier)
- data class App(override val identifier: String) : FormOrigin(identifier)
-
- companion object {
-
- private const val BUNDLE_KEY_WEB_IDENTIFIER = "webIdentifier"
- private const val BUNDLE_KEY_APP_IDENTIFIER = "appIdentifier"
-
- fun fromBundle(bundle: Bundle): FormOrigin? {
- val webIdentifier = bundle.getString(BUNDLE_KEY_WEB_IDENTIFIER)
- if (webIdentifier != null) {
- return Web(webIdentifier)
- } else {
- return App(bundle.getString(BUNDLE_KEY_APP_IDENTIFIER) ?: return null)
- }
- }
- }
-
- fun getPrettyIdentifier(context: Context, untrusted: Boolean = true) = when (this) {
- is Web -> identifier
- is App -> {
- val info = context.packageManager.getApplicationInfo(
- identifier, PackageManager.GET_META_DATA
- )
- val label = context.packageManager.getApplicationLabel(info)
- if (untrusted) "“$label”" else "$label"
- }
- }
-
- fun toBundle() = when (this) {
- is Web -> bundleOf(BUNDLE_KEY_WEB_IDENTIFIER to identifier)
- is App -> bundleOf(BUNDLE_KEY_APP_IDENTIFIER to identifier)
- }
-}
-
-/**
- * Manages the detection of fields to fill in an [AssistStructure] and determines the [FormOrigin].
- */
-@RequiresApi(Build.VERSION_CODES.O)
-private class Form(context: Context, structure: AssistStructure, isManualRequest: Boolean) {
-
- companion object {
-
- private val SUPPORTED_SCHEMES = listOf("http", "https")
- }
-
- private val relevantFields = mutableListOf()
- val ignoredIds = mutableListOf()
- private var fieldIndex = 0
-
- private var appPackage = structure.activityComponent.packageName
-
- private val trustedBrowserInfo =
- getBrowserAutofillSupportInfoIfTrusted(context, appPackage)
- val saveFlags = trustedBrowserInfo?.saveFlags
-
- private val webOrigins = mutableSetOf()
-
- init {
- d { "Request from $appPackage (${computeCertificatesHash(context, appPackage)})" }
- parseStructure(structure)
- }
-
- val scenario = detectFieldsToFill(isManualRequest)
- val formOrigin = determineFormOrigin(context)
-
- init {
- d { "Origin: $formOrigin" }
- }
-
- private fun parseStructure(structure: AssistStructure) {
- for (i in 0 until structure.windowNodeCount) {
- visitFormNode(structure.getWindowNodeAt(i).rootViewNode)
- }
- }
-
- private fun visitFormNode(node: AssistStructure.ViewNode, inheritedWebOrigin: String? = null) {
- trackOrigin(node)
- val field =
- if (trustedBrowserInfo?.multiOriginMethod == BrowserMultiOriginMethod.WebView) {
- FormField(node, fieldIndex, true, inheritedWebOrigin)
- } else {
- check(inheritedWebOrigin == null)
- FormField(node, fieldIndex, false)
- }
- if (field.relevantField) {
- d { "Relevant: $field" }
- relevantFields.add(field)
- fieldIndex++
- } else {
- d { "Ignored : $field" }
- ignoredIds.add(field.autofillId)
- }
- for (i in 0 until node.childCount) {
- visitFormNode(node.getChildAt(i), field.webOriginToPassDown)
- }
- }
-
- private fun detectFieldsToFill(isManualRequest: Boolean) = autofillStrategy.match(
- relevantFields,
- singleOriginMode = trustedBrowserInfo?.multiOriginMethod == BrowserMultiOriginMethod.None,
- isManualRequest = isManualRequest
- )
-
- private fun trackOrigin(node: AssistStructure.ViewNode) {
- if (trustedBrowserInfo == null) return
- node.webOrigin?.let {
- if (it !in webOrigins) {
- d { "Origin encountered: $it" }
- webOrigins.add(it)
- }
- }
- }
-
- private fun webOriginToFormOrigin(context: Context, origin: String): FormOrigin? {
- val uri = Uri.parse(origin) ?: return null
- val scheme = uri.scheme ?: return null
- if (scheme !in SUPPORTED_SCHEMES) return null
- val host = uri.host ?: return null
- return FormOrigin.Web(getPublicSuffixPlusOne(context, host))
- }
-
- private fun determineFormOrigin(context: Context): FormOrigin? {
- if (scenario == null) return null
- if (trustedBrowserInfo == null || webOrigins.isEmpty()) {
- // Security assumption: If a trusted browser includes no web origin in the provided
- // AssistStructure, then the form is a native browser form (e.g. for a sync password).
- // TODO: Support WebViews in apps via Digital Asset Links
- // See: https://developer.android.com/reference/android/service/autofill/AutofillService#web-security
- return FormOrigin.App(appPackage)
- }
- return when (trustedBrowserInfo.multiOriginMethod) {
- BrowserMultiOriginMethod.None -> {
- // Security assumption: If a browser is trusted but does not support tracking
- // multiple origins, it is expected to annotate a single field, in most cases its
- // URL bar, with a webOrigin. We err on the side of caution and only trust the
- // reported web origin if no other web origin appears on the page.
- webOriginToFormOrigin(context, webOrigins.singleOrNull() ?: return null)
- }
- BrowserMultiOriginMethod.WebView,
- BrowserMultiOriginMethod.Field -> {
- // Security assumption: For browsers with full autofill support (the `Field` case),
- // every form field is annotated with its origin. For browsers based on WebView,
- // this is true after the web origins of WebViews are passed down to their children.
- //
- // For browsers with the WebView or Field method of multi origin support, we take
- // the single origin among the detected fillable or saveable fields. If this origin
- // is null, but we encountered web origins elsewhere in the AssistStructure, the
- // situation is uncertain and Autofill should not be offered.
- webOriginToFormOrigin(
- context,
- scenario.allFields.map { it.webOrigin }.toSet().singleOrNull() ?: return null
- )
- }
- }
- }
-}
-
-/**
- * Represents a collection of fields in a specific app that can be filled or saved. This is the
- * entry point to all fill and save features.
- */
-@RequiresApi(Build.VERSION_CODES.O)
-class FillableForm private constructor(
- private val formOrigin: FormOrigin,
- private val scenario: AutofillScenario,
- private val ignoredIds: List,
- private val saveFlags: Int?
-) {
-
- companion object {
-
- fun makeFillInDataset(
- context: Context,
- credentials: Credentials,
- clientState: Bundle,
- action: AutofillAction
- ): Dataset {
- val remoteView = makePlaceholderRemoteView(context)
- val scenario = AutofillScenario.fromBundle(clientState)
- // Before Android P, Datasets used for fill-in had to come with a RemoteViews, even
- // though they are never shown.
- val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
- Dataset.Builder()
- } else {
- Dataset.Builder(remoteView)
- }
- return builder.run {
- if (scenario != null) fillWith(scenario, action, credentials)
- else e { "Failed to recover scenario from client state" }
- build()
- }
- }
-
- /**
- * Returns a [FillableForm] if a login form could be detected in [structure].
- */
- fun parseAssistStructure(
- context: Context,
- structure: AssistStructure,
- isManualRequest: Boolean
- ): FillableForm? {
- val form = Form(context, structure, isManualRequest)
- if (form.formOrigin == null || form.scenario == null) return null
- return FillableForm(
- form.formOrigin,
- form.scenario,
- form.ignoredIds,
- form.saveFlags
- )
- }
- }
-
- private val clientState = scenario.toBundle().apply {
- putAll(formOrigin.toBundle())
- }
-
- // We do not offer save when the only relevant field is a username field or there is no field.
- private val scenarioSupportsSave =
- scenario.fieldsToSave.minus(listOfNotNull(scenario.username)).isNotEmpty()
- private val canBeSaved = saveFlags != null && scenarioSupportsSave
-
- private fun makePlaceholderDataset(
- remoteView: RemoteViews,
- intentSender: IntentSender,
- action: AutofillAction
- ): Dataset {
- return Dataset.Builder(remoteView).run {
- fillWith(scenario, action, credentials = null)
- setAuthentication(intentSender)
- build()
- }
- }
-
- private fun makeMatchDataset(context: Context, file: File): Dataset? {
- if (scenario.fieldsToFillOn(AutofillAction.Match).isEmpty()) return null
- val remoteView = makeFillMatchRemoteView(context, file, formOrigin)
- val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context)
- return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Match)
- }
-
- private fun makeSearchDataset(context: Context): Dataset? {
- if (scenario.fieldsToFillOn(AutofillAction.Search).isEmpty()) return null
- val remoteView = makeSearchAndFillRemoteView(context, formOrigin)
- val intentSender =
- AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin)
- return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Search)
- }
-
- private fun makeGenerateDataset(context: Context): Dataset? {
- if (scenario.fieldsToFillOn(AutofillAction.Generate).isEmpty()) return null
- val remoteView = makeGenerateAndFillRemoteView(context, formOrigin)
- val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin)
- return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Generate)
- }
-
- private fun makeFillOtpFromSmsDataset(context: Context): Dataset? {
- if (scenario.fieldsToFillOn(AutofillAction.FillOtpFromSms).isEmpty()) return null
- if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null
- val remoteView = makeFillOtpFromSmsRemoteView(context, formOrigin)
- val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context)
- return makePlaceholderDataset(remoteView, intentSender, AutofillAction.FillOtpFromSms)
- }
-
- private fun makePublisherChangedDataset(
- context: Context,
- publisherChangedException: AutofillPublisherChangedException
- ): Dataset {
- val remoteView = makeWarningRemoteView(context)
- val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender(
- context, publisherChangedException
- )
- return makePlaceholderDataset(remoteView, intentSender, AutofillAction.Match)
- }
-
- private fun makePublisherChangedResponse(
- context: Context,
- publisherChangedException: AutofillPublisherChangedException
- ): FillResponse {
- return FillResponse.Builder().run {
- addDataset(makePublisherChangedDataset(context, publisherChangedException))
- setIgnoredIds(*ignoredIds.toTypedArray())
- build()
- }
- }
-
- // TODO: Support multi-step authentication flows in apps via FLAG_DELAY_SAVE
- // See: https://developer.android.com/reference/android/service/autofill/SaveInfo#FLAG_DELAY_SAVE
- private fun makeSaveInfo(): SaveInfo? {
- if (!canBeSaved) return null
- check(saveFlags != null)
- val idsToSave = scenario.fieldsToSave.map { it.autofillId }.toTypedArray()
- if (idsToSave.isEmpty()) return null
- var saveDataTypes = SaveInfo.SAVE_DATA_TYPE_PASSWORD
- if (scenario.username != null) {
- saveDataTypes = saveDataTypes or SaveInfo.SAVE_DATA_TYPE_USERNAME
- }
- return SaveInfo.Builder(saveDataTypes, idsToSave).run {
- setFlags(saveFlags)
- build()
- }
- }
-
- private fun makeFillResponse(context: Context, matchedFiles: List): FillResponse? {
- var hasDataset = false
- return FillResponse.Builder().run {
- for (file in matchedFiles) {
- makeMatchDataset(context, file)?.let {
- hasDataset = true
- addDataset(it)
- }
- }
- makeSearchDataset(context)?.let {
- hasDataset = true
- addDataset(it)
- }
- makeGenerateDataset(context)?.let {
- hasDataset = true
- addDataset(it)
- }
- makeFillOtpFromSmsDataset(context)?.let {
- hasDataset = true
- addDataset(it)
- }
- if (!hasDataset) return null
- makeSaveInfo()?.let { setSaveInfo(it) }
- setClientState(clientState)
- setIgnoredIds(*ignoredIds.toTypedArray())
- build()
- }
- }
-
- /**
- * Creates and returns a suitable [FillResponse] to the Autofill framework.
- */
- fun fillCredentials(context: Context, callback: FillCallback) {
- AutofillMatcher.getMatchesFor(context, formOrigin).fold(
- success = { matchedFiles ->
- callback.onSuccess(makeFillResponse(context, matchedFiles))
- },
- failure = { e ->
- e(e)
- callback.onSuccess(makePublisherChangedResponse(context, e))
- }
- )
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt
index fd2997d8..ecec6747 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt
@@ -4,6 +4,7 @@
*/
package com.zeapo.pwdstore.autofill.oreo
+import android.content.Context
import android.os.Build
import android.os.CancellationSignal
import android.service.autofill.AutofillService
@@ -15,10 +16,22 @@ import android.service.autofill.SaveRequest
import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.e
+import com.github.androidpasswordstore.autofillparser.AutofillScenario
+import com.github.androidpasswordstore.autofillparser.Credentials
+import com.github.androidpasswordstore.autofillparser.FillableForm
+import com.github.androidpasswordstore.autofillparser.FixedSaveCallback
+import com.github.androidpasswordstore.autofillparser.FormOrigin
+import com.github.androidpasswordstore.autofillparser.cachePublicSuffixList
+import com.github.androidpasswordstore.autofillparser.passwordValue
+import com.github.androidpasswordstore.autofillparser.recoverNodes
+import com.github.androidpasswordstore.autofillparser.usernameValue
import com.zeapo.pwdstore.BuildConfig
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity
+import com.zeapo.pwdstore.utils.PreferenceKeys
+import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.hasFlag
+import com.zeapo.pwdstore.utils.sharedPrefs
@RequiresApi(Build.VERSION_CODES.O)
class OreoAutofillService : AutofillService() {
@@ -66,13 +79,14 @@ class OreoAutofillService : AutofillService() {
}
val formToFill = FillableForm.parseAssistStructure(
this, structure,
- isManualRequest = request.flags hasFlag FillRequest.FLAG_MANUAL_REQUEST
+ isManualRequest = request.flags hasFlag FillRequest.FLAG_MANUAL_REQUEST,
+ getCustomSuffixes(),
) ?: run {
d { "Form cannot be filled" }
callback.onSuccess(null)
return
}
- formToFill.fillCredentials(this, callback)
+ AutofillResponseBuilder(formToFill).fillCredentials(this, callback)
}
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
@@ -113,3 +127,12 @@ class OreoAutofillService : AutofillService() {
)
}
}
+
+fun Context.getDefaultUsername() = sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)
+
+fun Context.getCustomSuffixes(): Sequence {
+ return sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
+ ?.splitToSequence('\n')
+ ?.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
+ ?: emptySequence()
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt
index 2c63c5c3..9bab9e6f 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt
@@ -22,11 +22,11 @@ import com.github.michaelbull.result.getOrElse
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.onSuccess
import com.github.michaelbull.result.runCatching
-import com.zeapo.pwdstore.autofill.oreo.AutofillAction
+import com.github.androidpasswordstore.autofillparser.AutofillAction
+import com.github.androidpasswordstore.autofillparser.Credentials
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
-import com.zeapo.pwdstore.autofill.oreo.Credentials
+import com.zeapo.pwdstore.autofill.oreo.AutofillResponseBuilder
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
-import com.zeapo.pwdstore.autofill.oreo.FillableForm
import com.zeapo.pwdstore.model.PasswordEntry
import com.zeapo.pwdstore.utils.OPENPGP_PROVIDER
import java.io.ByteArrayOutputStream
@@ -109,7 +109,7 @@ class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope {
setResult(RESULT_CANCELED)
} else {
val fillInDataset =
- FillableForm.makeFillInDataset(
+ AutofillResponseBuilder.makeFillInDataset(
this@AutofillDecryptActivity,
credentials,
clientState,
@@ -185,7 +185,7 @@ class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope {
@Suppress("BlockingMethodInNonBlockingContext")
PasswordEntry(decryptedOutput)
}
- Credentials.fromStoreEntry(this, file, entry, directoryStructure)
+ AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure)
}.getOrElse { e ->
e(e) { "Failed to parse password entry" }
return null
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt
index 7e29b061..95e49fdd 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt
@@ -24,6 +24,7 @@ import androidx.core.widget.addTextChangedListener
import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.LinearLayoutManager
import com.github.ajalt.timberkt.e
+import com.github.androidpasswordstore.autofillparser.FormOrigin
import com.zeapo.pwdstore.FilterMode
import com.zeapo.pwdstore.ListMode
import com.zeapo.pwdstore.R
@@ -33,7 +34,6 @@ import com.zeapo.pwdstore.SearchableRepositoryViewModel
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
-import com.zeapo.pwdstore.autofill.oreo.FormOrigin
import com.zeapo.pwdstore.databinding.ActivityOreoAutofillFilterBinding
import com.zeapo.pwdstore.utils.PasswordItem
import com.zeapo.pwdstore.utils.viewBinding
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt
index bcb27e65..44ed3446 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt
@@ -16,13 +16,13 @@ import android.text.format.DateUtils
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import com.github.ajalt.timberkt.e
+import com.github.androidpasswordstore.autofillparser.FormOrigin
+import com.github.androidpasswordstore.autofillparser.computeCertificatesHash
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
import com.zeapo.pwdstore.autofill.oreo.AutofillPublisherChangedException
-import com.zeapo.pwdstore.autofill.oreo.FormOrigin
-import com.zeapo.pwdstore.autofill.oreo.computeCertificatesHash
import com.zeapo.pwdstore.databinding.ActivityOreoAutofillPublisherChangedBinding
import com.zeapo.pwdstore.utils.viewBinding
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt
index 0052ff65..7f83d483 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt
@@ -16,12 +16,12 @@ import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.os.bundleOf
import com.github.ajalt.timberkt.e
-import com.zeapo.pwdstore.autofill.oreo.AutofillAction
+import com.github.androidpasswordstore.autofillparser.AutofillAction
+import com.github.androidpasswordstore.autofillparser.Credentials
+import com.github.androidpasswordstore.autofillparser.FormOrigin
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
-import com.zeapo.pwdstore.autofill.oreo.Credentials
-import com.zeapo.pwdstore.autofill.oreo.FillableForm
-import com.zeapo.pwdstore.autofill.oreo.FormOrigin
+import com.zeapo.pwdstore.autofill.oreo.AutofillResponseBuilder
import com.zeapo.pwdstore.crypto.PasswordCreationActivity
import com.zeapo.pwdstore.utils.PasswordRepository
import java.io.File
@@ -126,7 +126,7 @@ class AutofillSaveActivity : AppCompatActivity() {
return@registerForActivityResult
}
val credentials = Credentials(username, password, null)
- val fillInDataset = FillableForm.makeFillInDataset(
+ val fillInDataset = AutofillResponseBuilder.makeFillInDataset(
this,
credentials,
clientState,
diff --git a/app/src/nonFree/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt b/app/src/nonFree/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt
index 25a5ef93..43498c66 100644
--- a/app/src/nonFree/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt
+++ b/app/src/nonFree/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSmsActivity.kt
@@ -19,6 +19,8 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.e
import com.github.ajalt.timberkt.w
+import com.github.androidpasswordstore.autofillparser.AutofillAction
+import com.github.androidpasswordstore.autofillparser.Credentials
import com.github.michaelbull.result.onFailure
import com.github.michaelbull.result.runCatching
import com.google.android.gms.auth.api.phone.SmsCodeRetriever
@@ -27,9 +29,7 @@ import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.gms.common.api.ResolvableApiException
import com.google.android.gms.tasks.Task
-import com.zeapo.pwdstore.autofill.oreo.AutofillAction
-import com.zeapo.pwdstore.autofill.oreo.Credentials
-import com.zeapo.pwdstore.autofill.oreo.FillableForm
+import com.zeapo.pwdstore.autofill.oreo.AutofillResponseBuilder
import com.zeapo.pwdstore.databinding.ActivityOreoAutofillSmsBinding
import com.zeapo.pwdstore.utils.viewBinding
import java.util.concurrent.ExecutionException
@@ -145,13 +145,12 @@ class AutofillSmsActivity : AppCompatActivity() {
private val smsCodeRetrievedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val smsCode = intent.getStringExtra(SmsCodeRetriever.EXTRA_SMS_CODE)
- val fillInDataset =
- FillableForm.makeFillInDataset(
- this@AutofillSmsActivity,
- Credentials(null, null, smsCode),
- clientState,
- AutofillAction.FillOtpFromSms
- )
+ val fillInDataset = AutofillResponseBuilder.makeFillInDataset(
+ this@AutofillSmsActivity,
+ Credentials(null, null, smsCode),
+ clientState,
+ AutofillAction.FillOtpFromSms
+ )
setResult(RESULT_OK, Intent().apply {
putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset)
})
diff --git a/autofill-parser/.gitignore b/autofill-parser/.gitignore
new file mode 100644
index 00000000..796b96d1
--- /dev/null
+++ b/autofill-parser/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/autofill-parser/build.gradle.kts b/autofill-parser/build.gradle.kts
new file mode 100644
index 00000000..be90cd0a
--- /dev/null
+++ b/autofill-parser/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ kotlin("android")
+}
+
+android {
+ defaultConfig {
+ versionCode = 1
+ versionName = "1.0"
+ consumerProguardFiles("consumer-rules.pro")
+ }
+}
+
+dependencies {
+ implementation(Dependencies.AndroidX.core_ktx)
+ implementation(Dependencies.AndroidX.autofill)
+ implementation(Dependencies.Kotlin.Coroutines.android)
+ implementation(Dependencies.Kotlin.Coroutines.core)
+ implementation(Dependencies.ThirdParty.timberkt)
+}
diff --git a/autofill-parser/consumer-rules.pro b/autofill-parser/consumer-rules.pro
new file mode 100644
index 00000000..e69de29b
diff --git a/autofill-parser/proguard-rules.pro b/autofill-parser/proguard-rules.pro
new file mode 100644
index 00000000..f1b42451
--- /dev/null
+++ b/autofill-parser/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
diff --git a/autofill-parser/src/main/AndroidManifest.xml b/autofill-parser/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..42f0878d
--- /dev/null
+++ b/autofill-parser/src/main/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/app/src/main/assets/publicsuffixes b/autofill-parser/src/main/assets/publicsuffixes
similarity index 100%
rename from app/src/main/assets/publicsuffixes
rename to autofill-parser/src/main/assets/publicsuffixes
diff --git a/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillFormParser.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillFormParser.kt
new file mode 100644
index 00000000..30ff40d3
--- /dev/null
+++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillFormParser.kt
@@ -0,0 +1,217 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
+ */
+package com.github.androidpasswordstore.autofillparser
+
+import android.app.assist.AssistStructure
+import android.content.Context
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.view.autofill.AutofillId
+import androidx.annotation.RequiresApi
+import androidx.core.os.bundleOf
+import com.github.ajalt.timberkt.d
+
+/**
+ * A unique identifier for either an Android app (package name) or a website (origin minus port).
+ */
+sealed class FormOrigin(open val identifier: String) {
+
+ data class Web(override val identifier: String) : FormOrigin(identifier)
+ data class App(override val identifier: String) : FormOrigin(identifier)
+
+ companion object {
+
+ private const val BUNDLE_KEY_WEB_IDENTIFIER = "webIdentifier"
+ private const val BUNDLE_KEY_APP_IDENTIFIER = "appIdentifier"
+
+ fun fromBundle(bundle: Bundle): FormOrigin? {
+ val webIdentifier = bundle.getString(BUNDLE_KEY_WEB_IDENTIFIER)
+ if (webIdentifier != null) {
+ return Web(webIdentifier)
+ } else {
+ return App(bundle.getString(BUNDLE_KEY_APP_IDENTIFIER) ?: return null)
+ }
+ }
+ }
+
+ fun getPrettyIdentifier(context: Context, untrusted: Boolean = true) = when (this) {
+ is Web -> identifier
+ is App -> {
+ val info = context.packageManager.getApplicationInfo(
+ identifier, PackageManager.GET_META_DATA
+ )
+ val label = context.packageManager.getApplicationLabel(info)
+ if (untrusted) "“$label”" else "$label"
+ }
+ }
+
+ fun toBundle() = when (this) {
+ is Web -> bundleOf(BUNDLE_KEY_WEB_IDENTIFIER to identifier)
+ is App -> bundleOf(BUNDLE_KEY_APP_IDENTIFIER to identifier)
+ }
+}
+
+/**
+ * Manages the detection of fields to fill in an [AssistStructure] and determines the [FormOrigin].
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+private class AutofillFormParser(
+ context: Context,
+ structure: AssistStructure,
+ isManualRequest: Boolean,
+ private val customSuffixes: Sequence
+) {
+
+ companion object {
+ private val SUPPORTED_SCHEMES = listOf("http", "https")
+ }
+
+ private val relevantFields = mutableListOf()
+ val ignoredIds = mutableListOf()
+ private var fieldIndex = 0
+
+ private var appPackage = structure.activityComponent.packageName
+
+ private val trustedBrowserInfo =
+ getBrowserAutofillSupportInfoIfTrusted(context, appPackage)
+ val saveFlags = trustedBrowserInfo?.saveFlags
+
+ private val webOrigins = mutableSetOf()
+
+ init {
+ d { "Request from $appPackage (${computeCertificatesHash(context, appPackage)})" }
+ parseStructure(structure)
+ }
+
+ val scenario = detectFieldsToFill(isManualRequest)
+ val formOrigin = determineFormOrigin(context)
+
+ init {
+ d { "Origin: $formOrigin" }
+ }
+
+ private fun parseStructure(structure: AssistStructure) {
+ for (i in 0 until structure.windowNodeCount) {
+ visitFormNode(structure.getWindowNodeAt(i).rootViewNode)
+ }
+ }
+
+ private fun visitFormNode(node: AssistStructure.ViewNode, inheritedWebOrigin: String? = null) {
+ trackOrigin(node)
+ val field =
+ if (trustedBrowserInfo?.multiOriginMethod == BrowserMultiOriginMethod.WebView) {
+ FormField(node, fieldIndex, true, inheritedWebOrigin)
+ } else {
+ check(inheritedWebOrigin == null)
+ FormField(node, fieldIndex, false)
+ }
+ if (field.relevantField) {
+ d { "Relevant: $field" }
+ relevantFields.add(field)
+ fieldIndex++
+ } else {
+ d { "Ignored : $field" }
+ ignoredIds.add(field.autofillId)
+ }
+ for (i in 0 until node.childCount) {
+ visitFormNode(node.getChildAt(i), field.webOriginToPassDown)
+ }
+ }
+
+ private fun detectFieldsToFill(isManualRequest: Boolean) = autofillStrategy.match(
+ relevantFields,
+ singleOriginMode = trustedBrowserInfo?.multiOriginMethod == BrowserMultiOriginMethod.None,
+ isManualRequest = isManualRequest
+ )
+
+ private fun trackOrigin(node: AssistStructure.ViewNode) {
+ if (trustedBrowserInfo == null) return
+ node.webOrigin?.let {
+ if (it !in webOrigins) {
+ d { "Origin encountered: $it" }
+ webOrigins.add(it)
+ }
+ }
+ }
+
+ private fun webOriginToFormOrigin(context: Context, origin: String): FormOrigin? {
+ val uri = Uri.parse(origin) ?: return null
+ val scheme = uri.scheme ?: return null
+ if (scheme !in SUPPORTED_SCHEMES) return null
+ val host = uri.host ?: return null
+ return FormOrigin.Web(getPublicSuffixPlusOne(context, host, customSuffixes))
+ }
+
+ private fun determineFormOrigin(context: Context): FormOrigin? {
+ if (scenario == null) return null
+ if (trustedBrowserInfo == null || webOrigins.isEmpty()) {
+ // Security assumption: If a trusted browser includes no web origin in the provided
+ // AssistStructure, then the form is a native browser form (e.g. for a sync password).
+ // TODO: Support WebViews in apps via Digital Asset Links
+ // See: https://developer.android.com/reference/android/service/autofill/AutofillService#web-security
+ return FormOrigin.App(appPackage)
+ }
+ return when (trustedBrowserInfo.multiOriginMethod) {
+ BrowserMultiOriginMethod.None -> {
+ // Security assumption: If a browser is trusted but does not support tracking
+ // multiple origins, it is expected to annotate a single field, in most cases its
+ // URL bar, with a webOrigin. We err on the side of caution and only trust the
+ // reported web origin if no other web origin appears on the page.
+ webOriginToFormOrigin(context, webOrigins.singleOrNull() ?: return null)
+ }
+ BrowserMultiOriginMethod.WebView,
+ BrowserMultiOriginMethod.Field -> {
+ // Security assumption: For browsers with full autofill support (the `Field` case),
+ // every form field is annotated with its origin. For browsers based on WebView,
+ // this is true after the web origins of WebViews are passed down to their children.
+ //
+ // For browsers with the WebView or Field method of multi origin support, we take
+ // the single origin among the detected fillable or saveable fields. If this origin
+ // is null, but we encountered web origins elsewhere in the AssistStructure, the
+ // situation is uncertain and Autofill should not be offered.
+ webOriginToFormOrigin(
+ context,
+ scenario.allFields.map { it.webOrigin }.toSet().singleOrNull() ?: return null
+ )
+ }
+ }
+ }
+}
+
+data class Credentials(val username: String?, val password: String?, val otp: String?)
+
+/**
+ * Represents a collection of fields in a specific app that can be filled or saved. This is the
+ * entry point to all fill and save features.
+ */
+@RequiresApi(Build.VERSION_CODES.O)
+class FillableForm private constructor(
+ val formOrigin: FormOrigin,
+ val scenario: AutofillScenario,
+ val ignoredIds: List,
+ val saveFlags: Int?
+) {
+ companion object {
+ /**
+ * Returns a [FillableForm] if a login form could be detected in [structure].
+ */
+ fun parseAssistStructure(
+ context: Context,
+ structure: AssistStructure,
+ isManualRequest: Boolean,
+ customSuffixes: Sequence = emptySequence(),
+ ): FillableForm? {
+ val form = AutofillFormParser(context, structure, isManualRequest, customSuffixes)
+ if (form.formOrigin == null || form.scenario == null) return null
+ return FillableForm(form.formOrigin, form.scenario, form.ignoredIds, form.saveFlags)
+ }
+ }
+
+ fun toClientState() = scenario.toBundle().apply {
+ putAll(formOrigin.toBundle())
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillHelper.kt
similarity index 56%
rename from app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt
rename to autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillHelper.kt
index 502c9423..9273f432 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillHelper.kt
+++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillHelper.kt
@@ -1,8 +1,8 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
+ * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
*/
-package com.zeapo.pwdstore.autofill.oreo
+package com.github.androidpasswordstore.autofillparser
import android.annotation.SuppressLint
import android.app.assist.AssistStructure
@@ -13,18 +13,10 @@ import android.os.Build
import android.service.autofill.SaveCallback
import android.util.Base64
import android.view.autofill.AutofillId
-import android.widget.RemoteViews
import android.widget.Toast
import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e
-import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.model.PasswordEntry
-import com.zeapo.pwdstore.utils.PasswordRepository
-import com.zeapo.pwdstore.utils.PreferenceKeys
-import com.zeapo.pwdstore.utils.getString
-import com.zeapo.pwdstore.utils.sharedPrefs
-import java.io.File
import java.security.MessageDigest
private fun ByteArray.sha256(): ByteArray {
@@ -38,8 +30,6 @@ private fun ByteArray.base64(): String {
return Base64.encodeToString(this, Base64.NO_WRAP)
}
-private fun Context.getDefaultUsername() = sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_DEFAULT_USERNAME)
-
private fun stableHash(array: Collection): String {
val hashes = array.map { it.sha256().base64() }
return hashes.sorted().joinToString(separator = ";")
@@ -84,79 +74,6 @@ val AssistStructure.ViewNode.webOrigin: String?
"$scheme://$domain"
}
-data class Credentials(val username: String?, val password: String?, val otp: String?) {
- companion object {
-
- fun fromStoreEntry(
- context: Context,
- file: File,
- entry: PasswordEntry,
- directoryStructure: DirectoryStructure
- ): Credentials {
- // Always give priority to a username stored in the encrypted extras
- val username = entry.username
- ?: directoryStructure.getUsernameFor(file)
- ?: context.getDefaultUsername()
- return Credentials(username, entry.password, entry.calculateTotpCode())
- }
- }
-}
-
-private fun makeRemoteView(
- context: Context,
- title: String,
- summary: String,
- iconRes: Int
-): RemoteViews {
- return RemoteViews(context.packageName, R.layout.oreo_autofill_dataset).apply {
- setTextViewText(R.id.title, title)
- setTextViewText(R.id.summary, summary)
- setImageViewResource(R.id.icon, iconRes)
- }
-}
-
-fun makeFillMatchRemoteView(context: Context, file: File, formOrigin: FormOrigin): RemoteViews {
- val title = formOrigin.getPrettyIdentifier(context, untrusted = false)
- val directoryStructure = AutofillPreferences.directoryStructure(context)
- val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory())
- val summary = directoryStructure.getUsernameFor(relativeFile)
- ?: directoryStructure.getPathToIdentifierFor(relativeFile) ?: ""
- val iconRes = R.drawable.ic_person_black_24dp
- return makeRemoteView(context, title, summary, iconRes)
-}
-
-fun makeSearchAndFillRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews {
- val title = formOrigin.getPrettyIdentifier(context, untrusted = true)
- val summary = context.getString(R.string.oreo_autofill_search_in_store)
- val iconRes = R.drawable.ic_search_black_24dp
- return makeRemoteView(context, title, summary, iconRes)
-}
-
-fun makeGenerateAndFillRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews {
- val title = formOrigin.getPrettyIdentifier(context, untrusted = true)
- val summary = context.getString(R.string.oreo_autofill_generate_password)
- val iconRes = R.drawable.ic_autofill_new_password
- return makeRemoteView(context, title, summary, iconRes)
-}
-
-fun makeFillOtpFromSmsRemoteView(context: Context, formOrigin: FormOrigin): RemoteViews {
- val title = formOrigin.getPrettyIdentifier(context, untrusted = true)
- val summary = context.getString(R.string.oreo_autofill_fill_otp_from_sms)
- val iconRes = R.drawable.ic_autofill_sms
- return makeRemoteView(context, title, summary, iconRes)
-}
-
-fun makePlaceholderRemoteView(context: Context): RemoteViews {
- return makeRemoteView(context, "PLACEHOLDER", "PLACEHOLDER", R.mipmap.ic_launcher)
-}
-
-fun makeWarningRemoteView(context: Context): RemoteViews {
- val title = context.getString(R.string.oreo_autofill_warning_publisher_dataset_title)
- val summary = context.getString(R.string.oreo_autofill_warning_publisher_dataset_summary)
- val iconRes = R.drawable.ic_warning_red_24dp
- return makeRemoteView(context, title, summary, iconRes)
-}
-
@RequiresApi(Build.VERSION_CODES.O)
class FixedSaveCallback(context: Context, private val callback: SaveCallback) {
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillScenario.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillScenario.kt
similarity index 97%
rename from app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillScenario.kt
rename to autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillScenario.kt
index f4fd5cf1..a374bc37 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillScenario.kt
+++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillScenario.kt
@@ -1,8 +1,8 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
+ * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
*/
-package com.zeapo.pwdstore.autofill.oreo
+package com.github.androidpasswordstore.autofillparser
import android.app.assist.AssistStructure
import android.os.Build
@@ -12,8 +12,6 @@ import android.view.autofill.AutofillId
import android.view.autofill.AutofillValue
import androidx.annotation.RequiresApi
import com.github.ajalt.timberkt.e
-import com.github.michaelbull.result.getOrElse
-import com.github.michaelbull.result.runCatching
enum class AutofillAction {
Match, Search, Generate, FillOtpFromSms
@@ -38,7 +36,7 @@ sealed class AutofillScenario {
const val BUNDLE_KEY_GENERIC_PASSWORD_IDS = "genericPasswordIds"
fun fromBundle(clientState: Bundle): AutofillScenario? {
- return runCatching {
+ return try {
Builder().apply {
username = clientState.getParcelable(BUNDLE_KEY_USERNAME_ID)
fillUsername = clientState.getBoolean(BUNDLE_KEY_FILL_USERNAME)
@@ -59,7 +57,7 @@ sealed class AutofillScenario {
) ?: emptyList()
)
}.build()
- }.getOrElse { e ->
+ } catch(e: Throwable) {
e(e)
null
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillStrategy.kt
similarity index 96%
rename from app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt
rename to autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillStrategy.kt
index 90bb7051..b8356783 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategy.kt
+++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillStrategy.kt
@@ -1,13 +1,13 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
+ * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
*/
-package com.zeapo.pwdstore.autofill.oreo
+package com.github.androidpasswordstore.autofillparser
import android.os.Build
import androidx.annotation.RequiresApi
-import com.zeapo.pwdstore.autofill.oreo.CertaintyLevel.Certain
-import com.zeapo.pwdstore.autofill.oreo.CertaintyLevel.Likely
+import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Certain
+import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Likely
private inline fun Pair.all(predicate: T.() -> Boolean) =
predicate(first) && predicate(second)
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillStrategyDsl.kt
similarity index 99%
rename from app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt
rename to autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillStrategyDsl.kt
index cae84d54..86201be8 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillStrategyDsl.kt
+++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/AutofillStrategyDsl.kt
@@ -1,8 +1,8 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
+ * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
*/
-package com.zeapo.pwdstore.autofill.oreo
+package com.github.androidpasswordstore.autofillparser
import android.os.Build
import androidx.annotation.RequiresApi
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt
similarity index 98%
rename from app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt
rename to autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt
index e49f7e81..b243a4c0 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt
+++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FeatureAndTrustDetection.kt
@@ -1,8 +1,8 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
+ * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
*/
-package com.zeapo.pwdstore.autofill.oreo
+package com.github.androidpasswordstore.autofillparser
import android.content.Context
import android.content.Intent
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FormField.kt
similarity index 99%
rename from app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt
rename to autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FormField.kt
index 6435a83f..ae16a995 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FormField.kt
+++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/FormField.kt
@@ -1,8 +1,8 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
+ * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
*/
-package com.zeapo.pwdstore.autofill.oreo
+package com.github.androidpasswordstore.autofillparser
import android.app.assist.AssistStructure
import android.os.Build
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/PublicSuffixListCache.kt b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/PublicSuffixListCache.kt
similarity index 78%
rename from app/src/main/java/com/zeapo/pwdstore/autofill/oreo/PublicSuffixListCache.kt
rename to autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/PublicSuffixListCache.kt
index 8107248e..316d102b 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/PublicSuffixListCache.kt
+++ b/autofill-parser/src/main/java/com/github/androidpasswordstore/autofillparser/PublicSuffixListCache.kt
@@ -1,14 +1,11 @@
/*
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
+ * SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
*/
-package com.zeapo.pwdstore.autofill.oreo
+package com.github.androidpasswordstore.autofillparser
import android.content.Context
import android.util.Patterns
-import com.zeapo.pwdstore.utils.PreferenceKeys
-import com.zeapo.pwdstore.utils.getString
-import com.zeapo.pwdstore.utils.sharedPrefs
import kotlinx.coroutines.runBlocking
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
@@ -38,7 +35,7 @@ fun cachePublicSuffixList(context: Context) {
* Note: Invalid domains, such as IP addresses, are returned unchanged and thus never collide with
* the return value for valid domains.
*/
-fun getPublicSuffixPlusOne(context: Context, domain: String) = runBlocking {
+fun getPublicSuffixPlusOne(context: Context, domain: String, customSuffixes: Sequence) = runBlocking {
// We only feed valid domain names which are not IP addresses into getPublicSuffixPlusOne.
// We do not check whether the domain actually exists (actually, not even whether its TLD
// exists). As long as we restrict ourselves to syntactically valid domain names,
@@ -48,7 +45,7 @@ fun getPublicSuffixPlusOne(context: Context, domain: String) = runBlocking {
) {
domain
} else {
- getCanonicalSuffix(context, domain)
+ getCanonicalSuffix(context, domain, customSuffixes)
}
}
@@ -68,19 +65,12 @@ fun getSuffixPlusUpToOne(domain: String, suffix: String): String? {
return "$lastPrefixPart.$suffix"
}
-fun getCustomSuffixes(context: Context): Sequence {
- return context.sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
- ?.splitToSequence('\n')
- ?.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
- ?: emptySequence()
-}
-
-suspend fun getCanonicalSuffix(context: Context, domain: String): String {
+suspend fun getCanonicalSuffix(context: Context, domain: String, customSuffixes: Sequence): String {
val publicSuffixList = PublicSuffixListCache.getOrCachePublicSuffixList(context)
val publicSuffixPlusOne = publicSuffixList.getPublicSuffixPlusOne(domain).await()
?: return domain
var longestSuffix = publicSuffixPlusOne
- for (customSuffix in getCustomSuffixes(context)) {
+ for (customSuffix in customSuffixes) {
val suffixPlusUpToOne = getSuffixPlusUpToOne(domain, customSuffix) ?: continue
// A shorter suffix is automatically a substring.
if (suffixPlusUpToOne.length > longestSuffix.length)
diff --git a/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt b/autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt
similarity index 98%
rename from app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt
rename to autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt
index 0fb59002..d4d1c1af 100644
--- a/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt
+++ b/autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixList.kt
@@ -1,5 +1,5 @@
/*
- * SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0
+ * SPDX-License-Identifier: (LGPL-3.0-only WITH LGPL-3.0-linking-exception) OR MPL-2.0
*/
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
diff --git a/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt b/autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt
similarity index 98%
rename from app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt
rename to autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt
index 778e9fee..595d46b1 100644
--- a/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt
+++ b/autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListData.kt
@@ -1,5 +1,5 @@
/*
- * SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0
+ * SPDX-License-Identifier: (LGPL-3.0-only WITH LGPL-3.0-linking-exception) OR MPL-2.0
*/
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
diff --git a/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt b/autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt
similarity index 94%
rename from app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt
rename to autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt
index 65caeae5..d8542f94 100644
--- a/app/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt
+++ b/autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/PublicSuffixListLoader.kt
@@ -1,5 +1,5 @@
/*
- * SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0
+ * SPDX-License-Identifier: (LGPL-3.0-only WITH LGPL-3.0-linking-exception) OR MPL-2.0
*/
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
diff --git a/app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt b/autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt
similarity index 97%
rename from app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt
rename to autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt
index 43fb7ab1..fbe8ec5c 100644
--- a/app/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt
+++ b/autofill-parser/src/main/java/mozilla/components/lib/publicsuffixlist/ext/ByteArray.kt
@@ -1,5 +1,5 @@
/*
- * SPDX-License-Identifier: GPL-3.0-only OR MPL-2.0
+ * SPDX-License-Identifier: (LGPL-3.0-only WITH LGPL-3.0-linking-exception) OR MPL-2.0
*/
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
diff --git a/buildSrc/src/main/java/Dependencies.kt b/buildSrc/src/main/java/Dependencies.kt
index 0c42107c..52c79bc0 100644
--- a/buildSrc/src/main/java/Dependencies.kt
+++ b/buildSrc/src/main/java/Dependencies.kt
@@ -49,6 +49,7 @@ object Dependencies {
const val openpgp_ktx = "com.github.android-password-store:openpgp-ktx:2.1.0"
const val zxing_android_embedded = "com.github.android-password-store:zxing-android-embedded:4.1.0-aps"
+ const val autofill_parser = ":autofill-parser"
}
object ThirdParty {
diff --git a/settings.gradle.kts b/settings.gradle.kts
index a418994b..04d7465d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -2,4 +2,5 @@
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
+include(":autofill-parser")
include(":app")