Autofill: Extract AutofillParser into separate subproject (#1101)
Co-authored-by: Harsh Shandilya <me@msfjarvis.dev> Co-authored-by: Fabian Henneke <fabian@henneke.me>
This commit is contained in:
parent
4ba3b75f85
commit
0810273444
36 changed files with 623 additions and 536 deletions
|
@ -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
|
||||
|
|
|
@ -12,11 +12,11 @@
|
|||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
<option value="$PROJECT_DIR$/autofill-parser" />
|
||||
<option value="$PROJECT_DIR$/buildSrc" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
<option name="useQualifiedModuleNames" value="true" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<File>): 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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<FormField>()
|
||||
val ignoredIds = mutableListOf<AutofillId>()
|
||||
private var fieldIndex = 0
|
||||
|
||||
private var appPackage = structure.activityComponent.packageName
|
||||
|
||||
private val trustedBrowserInfo =
|
||||
getBrowserAutofillSupportInfoIfTrusted(context, appPackage)
|
||||
val saveFlags = trustedBrowserInfo?.saveFlags
|
||||
|
||||
private val webOrigins = mutableSetOf<String>()
|
||||
|
||||
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<FormField>,
|
||||
private val ignoredIds: List<AutofillId>,
|
||||
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<File>): 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))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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<String> {
|
||||
return sharedPrefs.getString(PreferenceKeys.OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES)
|
||||
?.splitToSequence('\n')
|
||||
?.filter { it.isNotBlank() && it.first() != '.' && it.last() != '.' }
|
||||
?: emptySequence()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
|
|
1
autofill-parser/.gitignore
vendored
Normal file
1
autofill-parser/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
19
autofill-parser/build.gradle.kts
Normal file
19
autofill-parser/build.gradle.kts
Normal file
|
@ -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)
|
||||
}
|
0
autofill-parser/consumer-rules.pro
Normal file
0
autofill-parser/consumer-rules.pro
Normal file
21
autofill-parser/proguard-rules.pro
vendored
Normal file
21
autofill-parser/proguard-rules.pro
vendored
Normal file
|
@ -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
|
7
autofill-parser/src/main/AndroidManifest.xml
Normal file
7
autofill-parser/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||
~ SPDX-License-Identifier: LGPL-3.0-only WITH LGPL-3.0-linking-exception
|
||||
-->
|
||||
|
||||
<manifest package="com.github.androidpasswordstore.autofillparser">
|
||||
</manifest>
|
|
@ -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<String>
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val SUPPORTED_SCHEMES = listOf("http", "https")
|
||||
}
|
||||
|
||||
private val relevantFields = mutableListOf<FormField>()
|
||||
val ignoredIds = mutableListOf<AutofillId>()
|
||||
private var fieldIndex = 0
|
||||
|
||||
private var appPackage = structure.activityComponent.packageName
|
||||
|
||||
private val trustedBrowserInfo =
|
||||
getBrowserAutofillSupportInfoIfTrusted(context, appPackage)
|
||||
val saveFlags = trustedBrowserInfo?.saveFlags
|
||||
|
||||
private val webOrigins = mutableSetOf<String>()
|
||||
|
||||
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<FormField>,
|
||||
val ignoredIds: List<AutofillId>,
|
||||
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<String> = 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())
|
||||
}
|
||||
}
|
|
@ -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<ByteArray>): 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) {
|
||||
|
|
@ -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<out T : Any> {
|
|||
const val BUNDLE_KEY_GENERIC_PASSWORD_IDS = "genericPasswordIds"
|
||||
|
||||
fun fromBundle(clientState: Bundle): AutofillScenario<AutofillId>? {
|
||||
return runCatching {
|
||||
return try {
|
||||
Builder<AutofillId>().apply {
|
||||
username = clientState.getParcelable(BUNDLE_KEY_USERNAME_ID)
|
||||
fillUsername = clientState.getBoolean(BUNDLE_KEY_FILL_USERNAME)
|
||||
|
@ -59,7 +57,7 @@ sealed class AutofillScenario<out T : Any> {
|
|||
) ?: emptyList()
|
||||
)
|
||||
}.build()
|
||||
}.getOrElse { e ->
|
||||
} catch(e: Throwable) {
|
||||
e(e)
|
||||
null
|
||||
}
|
|
@ -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 <T> Pair<T, T>.all(predicate: T.() -> Boolean) =
|
||||
predicate(first) && predicate(second)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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<String>) = 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<String> {
|
||||
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>): 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)
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue