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")