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:
Vincent Breitmoser 2020-09-16 20:17:55 +02:00 committed by GitHub
parent 4ba3b75f85
commit 0810273444
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 623 additions and 536 deletions

View file

@ -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

View file

@ -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>

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View 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())
}
}

View file

@ -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()
}
}
}
}

View file

@ -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)
}

View file

@ -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))
}
)
}
}

View file

@ -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()
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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,8 +145,7 @@ 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(
val fillInDataset = AutofillResponseBuilder.makeFillInDataset(
this@AutofillSmsActivity,
Credentials(null, null, smsCode),
clientState,

1
autofill-parser/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View 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)
}

View file

21
autofill-parser/proguard-rules.pro vendored Normal file
View 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

View 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>

View file

@ -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())
}
}

View file

@ -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) {

View file

@ -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
}

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

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