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
|
git config user.email noreply@github.com
|
||||||
|
|
||||||
- name: Download new publicsuffix data
|
- 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
|
- name: Compare list changes
|
||||||
run: if [[ $(git diff --binary --stat) != '' ]]; then echo "::set-env name=UPDATED::true"; fi
|
run: if [[ $(git diff --binary --stat) != '' ]]; then echo "::set-env name=UPDATED::true"; fi
|
||||||
|
|
|
@ -12,11 +12,11 @@
|
||||||
<set>
|
<set>
|
||||||
<option value="$PROJECT_DIR$" />
|
<option value="$PROJECT_DIR$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
<option value="$PROJECT_DIR$/autofill-parser" />
|
||||||
<option value="$PROJECT_DIR$/buildSrc" />
|
<option value="$PROJECT_DIR$/buildSrc" />
|
||||||
</set>
|
</set>
|
||||||
</option>
|
</option>
|
||||||
<option name="resolveModulePerSourceSet" value="false" />
|
<option name="resolveModulePerSourceSet" value="false" />
|
||||||
<option name="useQualifiedModuleNames" value="true" />
|
|
||||||
</GradleProjectSettings>
|
</GradleProjectSettings>
|
||||||
</option>
|
</option>
|
||||||
</component>
|
</component>
|
||||||
|
|
|
@ -89,7 +89,6 @@ android {
|
||||||
dependencies {
|
dependencies {
|
||||||
compileOnly(Dependencies.AndroidX.annotation)
|
compileOnly(Dependencies.AndroidX.annotation)
|
||||||
implementation(Dependencies.AndroidX.activity_ktx)
|
implementation(Dependencies.AndroidX.activity_ktx)
|
||||||
implementation(Dependencies.AndroidX.autofill)
|
|
||||||
implementation(Dependencies.AndroidX.appcompat)
|
implementation(Dependencies.AndroidX.appcompat)
|
||||||
implementation(Dependencies.AndroidX.biometric)
|
implementation(Dependencies.AndroidX.biometric)
|
||||||
implementation(Dependencies.AndroidX.constraint_layout)
|
implementation(Dependencies.AndroidX.constraint_layout)
|
||||||
|
@ -109,6 +108,7 @@ dependencies {
|
||||||
implementation(Dependencies.Kotlin.Coroutines.android)
|
implementation(Dependencies.Kotlin.Coroutines.android)
|
||||||
implementation(Dependencies.Kotlin.Coroutines.core)
|
implementation(Dependencies.Kotlin.Coroutines.core)
|
||||||
|
|
||||||
|
implementation(project(Dependencies.FirstParty.autofill_parser))
|
||||||
implementation(Dependencies.FirstParty.openpgp_ktx)
|
implementation(Dependencies.FirstParty.openpgp_ktx)
|
||||||
implementation(Dependencies.FirstParty.zxing_android_embedded)
|
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.e
|
||||||
import com.github.ajalt.timberkt.i
|
import com.github.ajalt.timberkt.i
|
||||||
import com.github.ajalt.timberkt.w
|
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.fold
|
||||||
import com.github.michaelbull.result.getOr
|
import com.github.michaelbull.result.getOr
|
||||||
import com.github.michaelbull.result.onFailure
|
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.snackbar.Snackbar
|
||||||
import com.google.android.material.textfield.TextInputEditText
|
import com.google.android.material.textfield.TextInputEditText
|
||||||
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
|
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.BasePgpActivity.Companion.getLongName
|
||||||
import com.zeapo.pwdstore.crypto.DecryptActivity
|
import com.zeapo.pwdstore.crypto.DecryptActivity
|
||||||
import com.zeapo.pwdstore.crypto.PasswordCreationActivity
|
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.Timber.tag
|
||||||
import com.github.ajalt.timberkt.d
|
import com.github.ajalt.timberkt.d
|
||||||
import com.github.ajalt.timberkt.w
|
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.getOr
|
||||||
import com.github.michaelbull.result.onFailure
|
import com.github.michaelbull.result.onFailure
|
||||||
import com.github.michaelbull.result.runCatching
|
import com.github.michaelbull.result.runCatching
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.zeapo.pwdstore.autofill.AutofillPreferenceActivity
|
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.crypto.BasePgpActivity
|
||||||
import com.zeapo.pwdstore.git.GitConfigActivity
|
import com.zeapo.pwdstore.git.GitConfigActivity
|
||||||
import com.zeapo.pwdstore.git.GitServerConfigActivity
|
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.Err
|
||||||
import com.github.michaelbull.result.Ok
|
import com.github.michaelbull.result.Ok
|
||||||
import com.github.michaelbull.result.Result
|
import com.github.michaelbull.result.Result
|
||||||
|
import com.github.androidpasswordstore.autofillparser.FormOrigin
|
||||||
|
import com.github.androidpasswordstore.autofillparser.computeCertificatesHash
|
||||||
import com.zeapo.pwdstore.R
|
import com.zeapo.pwdstore.R
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,8 @@ package com.zeapo.pwdstore.autofill.oreo
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.github.androidpasswordstore.autofillparser.Credentials
|
||||||
|
import com.zeapo.pwdstore.model.PasswordEntry
|
||||||
import com.zeapo.pwdstore.utils.sharedPrefs
|
import com.zeapo.pwdstore.utils.sharedPrefs
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.file.Paths
|
import java.nio.file.Paths
|
||||||
|
@ -121,4 +123,17 @@ object AutofillPreferences {
|
||||||
val value = context.sharedPrefs.getString(DirectoryStructure.PREFERENCE, null)
|
val value = context.sharedPrefs.getString(DirectoryStructure.PREFERENCE, null)
|
||||||
return DirectoryStructure.fromValue(value)
|
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
|
package com.zeapo.pwdstore.autofill.oreo
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.CancellationSignal
|
import android.os.CancellationSignal
|
||||||
import android.service.autofill.AutofillService
|
import android.service.autofill.AutofillService
|
||||||
|
@ -15,10 +16,22 @@ import android.service.autofill.SaveRequest
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.github.ajalt.timberkt.d
|
import com.github.ajalt.timberkt.d
|
||||||
import com.github.ajalt.timberkt.e
|
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.BuildConfig
|
||||||
import com.zeapo.pwdstore.R
|
import com.zeapo.pwdstore.R
|
||||||
import com.zeapo.pwdstore.autofill.oreo.ui.AutofillSaveActivity
|
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.hasFlag
|
||||||
|
import com.zeapo.pwdstore.utils.sharedPrefs
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.O)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
class OreoAutofillService : AutofillService() {
|
class OreoAutofillService : AutofillService() {
|
||||||
|
@ -66,13 +79,14 @@ class OreoAutofillService : AutofillService() {
|
||||||
}
|
}
|
||||||
val formToFill = FillableForm.parseAssistStructure(
|
val formToFill = FillableForm.parseAssistStructure(
|
||||||
this, structure,
|
this, structure,
|
||||||
isManualRequest = request.flags hasFlag FillRequest.FLAG_MANUAL_REQUEST
|
isManualRequest = request.flags hasFlag FillRequest.FLAG_MANUAL_REQUEST,
|
||||||
|
getCustomSuffixes(),
|
||||||
) ?: run {
|
) ?: run {
|
||||||
d { "Form cannot be filled" }
|
d { "Form cannot be filled" }
|
||||||
callback.onSuccess(null)
|
callback.onSuccess(null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
formToFill.fillCredentials(this, callback)
|
AutofillResponseBuilder(formToFill).fillCredentials(this, callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) {
|
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.onFailure
|
||||||
import com.github.michaelbull.result.onSuccess
|
import com.github.michaelbull.result.onSuccess
|
||||||
import com.github.michaelbull.result.runCatching
|
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.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.DirectoryStructure
|
||||||
import com.zeapo.pwdstore.autofill.oreo.FillableForm
|
|
||||||
import com.zeapo.pwdstore.model.PasswordEntry
|
import com.zeapo.pwdstore.model.PasswordEntry
|
||||||
import com.zeapo.pwdstore.utils.OPENPGP_PROVIDER
|
import com.zeapo.pwdstore.utils.OPENPGP_PROVIDER
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
@ -109,7 +109,7 @@ class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope {
|
||||||
setResult(RESULT_CANCELED)
|
setResult(RESULT_CANCELED)
|
||||||
} else {
|
} else {
|
||||||
val fillInDataset =
|
val fillInDataset =
|
||||||
FillableForm.makeFillInDataset(
|
AutofillResponseBuilder.makeFillInDataset(
|
||||||
this@AutofillDecryptActivity,
|
this@AutofillDecryptActivity,
|
||||||
credentials,
|
credentials,
|
||||||
clientState,
|
clientState,
|
||||||
|
@ -185,7 +185,7 @@ class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope {
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
PasswordEntry(decryptedOutput)
|
PasswordEntry(decryptedOutput)
|
||||||
}
|
}
|
||||||
Credentials.fromStoreEntry(this, file, entry, directoryStructure)
|
AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure)
|
||||||
}.getOrElse { e ->
|
}.getOrElse { e ->
|
||||||
e(e) { "Failed to parse password entry" }
|
e(e) { "Failed to parse password entry" }
|
||||||
return null
|
return null
|
||||||
|
|
|
@ -24,6 +24,7 @@ import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.github.ajalt.timberkt.e
|
import com.github.ajalt.timberkt.e
|
||||||
|
import com.github.androidpasswordstore.autofillparser.FormOrigin
|
||||||
import com.zeapo.pwdstore.FilterMode
|
import com.zeapo.pwdstore.FilterMode
|
||||||
import com.zeapo.pwdstore.ListMode
|
import com.zeapo.pwdstore.ListMode
|
||||||
import com.zeapo.pwdstore.R
|
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.AutofillMatcher
|
||||||
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
|
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
|
||||||
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
|
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
|
||||||
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
|
|
||||||
import com.zeapo.pwdstore.databinding.ActivityOreoAutofillFilterBinding
|
import com.zeapo.pwdstore.databinding.ActivityOreoAutofillFilterBinding
|
||||||
import com.zeapo.pwdstore.utils.PasswordItem
|
import com.zeapo.pwdstore.utils.PasswordItem
|
||||||
import com.zeapo.pwdstore.utils.viewBinding
|
import com.zeapo.pwdstore.utils.viewBinding
|
||||||
|
|
|
@ -16,13 +16,13 @@ import android.text.format.DateUtils
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import com.github.ajalt.timberkt.e
|
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.onFailure
|
||||||
import com.github.michaelbull.result.runCatching
|
import com.github.michaelbull.result.runCatching
|
||||||
import com.zeapo.pwdstore.R
|
import com.zeapo.pwdstore.R
|
||||||
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
|
import com.zeapo.pwdstore.autofill.oreo.AutofillMatcher
|
||||||
import com.zeapo.pwdstore.autofill.oreo.AutofillPublisherChangedException
|
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.databinding.ActivityOreoAutofillPublisherChangedBinding
|
||||||
import com.zeapo.pwdstore.utils.viewBinding
|
import com.zeapo.pwdstore.utils.viewBinding
|
||||||
|
|
||||||
|
|
|
@ -16,12 +16,12 @@ import androidx.annotation.RequiresApi
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import com.github.ajalt.timberkt.e
|
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.AutofillMatcher
|
||||||
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
|
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.FillableForm
|
|
||||||
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
|
|
||||||
import com.zeapo.pwdstore.crypto.PasswordCreationActivity
|
import com.zeapo.pwdstore.crypto.PasswordCreationActivity
|
||||||
import com.zeapo.pwdstore.utils.PasswordRepository
|
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -126,7 +126,7 @@ class AutofillSaveActivity : AppCompatActivity() {
|
||||||
return@registerForActivityResult
|
return@registerForActivityResult
|
||||||
}
|
}
|
||||||
val credentials = Credentials(username, password, null)
|
val credentials = Credentials(username, password, null)
|
||||||
val fillInDataset = FillableForm.makeFillInDataset(
|
val fillInDataset = AutofillResponseBuilder.makeFillInDataset(
|
||||||
this,
|
this,
|
||||||
credentials,
|
credentials,
|
||||||
clientState,
|
clientState,
|
||||||
|
|
|
@ -19,6 +19,8 @@ import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import com.github.ajalt.timberkt.e
|
import com.github.ajalt.timberkt.e
|
||||||
import com.github.ajalt.timberkt.w
|
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.onFailure
|
||||||
import com.github.michaelbull.result.runCatching
|
import com.github.michaelbull.result.runCatching
|
||||||
import com.google.android.gms.auth.api.phone.SmsCodeRetriever
|
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.GoogleApiAvailability
|
||||||
import com.google.android.gms.common.api.ResolvableApiException
|
import com.google.android.gms.common.api.ResolvableApiException
|
||||||
import com.google.android.gms.tasks.Task
|
import com.google.android.gms.tasks.Task
|
||||||
import com.zeapo.pwdstore.autofill.oreo.AutofillAction
|
import com.zeapo.pwdstore.autofill.oreo.AutofillResponseBuilder
|
||||||
import com.zeapo.pwdstore.autofill.oreo.Credentials
|
|
||||||
import com.zeapo.pwdstore.autofill.oreo.FillableForm
|
|
||||||
import com.zeapo.pwdstore.databinding.ActivityOreoAutofillSmsBinding
|
import com.zeapo.pwdstore.databinding.ActivityOreoAutofillSmsBinding
|
||||||
import com.zeapo.pwdstore.utils.viewBinding
|
import com.zeapo.pwdstore.utils.viewBinding
|
||||||
import java.util.concurrent.ExecutionException
|
import java.util.concurrent.ExecutionException
|
||||||
|
@ -145,8 +145,7 @@ class AutofillSmsActivity : AppCompatActivity() {
|
||||||
private val smsCodeRetrievedReceiver = object : BroadcastReceiver() {
|
private val smsCodeRetrievedReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
val smsCode = intent.getStringExtra(SmsCodeRetriever.EXTRA_SMS_CODE)
|
val smsCode = intent.getStringExtra(SmsCodeRetriever.EXTRA_SMS_CODE)
|
||||||
val fillInDataset =
|
val fillInDataset = AutofillResponseBuilder.makeFillInDataset(
|
||||||
FillableForm.makeFillInDataset(
|
|
||||||
this@AutofillSmsActivity,
|
this@AutofillSmsActivity,
|
||||||
Credentials(null, null, smsCode),
|
Credentials(null, null, smsCode),
|
||||||
clientState,
|
clientState,
|
||||||
|
|
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.
|
* 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.annotation.SuppressLint
|
||||||
import android.app.assist.AssistStructure
|
import android.app.assist.AssistStructure
|
||||||
|
@ -13,18 +13,10 @@ import android.os.Build
|
||||||
import android.service.autofill.SaveCallback
|
import android.service.autofill.SaveCallback
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.view.autofill.AutofillId
|
import android.view.autofill.AutofillId
|
||||||
import android.widget.RemoteViews
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.github.ajalt.timberkt.Timber.tag
|
import com.github.ajalt.timberkt.Timber.tag
|
||||||
import com.github.ajalt.timberkt.e
|
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
|
import java.security.MessageDigest
|
||||||
|
|
||||||
private fun ByteArray.sha256(): ByteArray {
|
private fun ByteArray.sha256(): ByteArray {
|
||||||
|
@ -38,8 +30,6 @@ private fun ByteArray.base64(): String {
|
||||||
return Base64.encodeToString(this, Base64.NO_WRAP)
|
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 {
|
private fun stableHash(array: Collection<ByteArray>): String {
|
||||||
val hashes = array.map { it.sha256().base64() }
|
val hashes = array.map { it.sha256().base64() }
|
||||||
return hashes.sorted().joinToString(separator = ";")
|
return hashes.sorted().joinToString(separator = ";")
|
||||||
|
@ -84,79 +74,6 @@ val AssistStructure.ViewNode.webOrigin: String?
|
||||||
"$scheme://$domain"
|
"$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)
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
class FixedSaveCallback(context: Context, private val callback: SaveCallback) {
|
class FixedSaveCallback(context: Context, private val callback: SaveCallback) {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
* 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.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
@ -12,8 +12,6 @@ import android.view.autofill.AutofillId
|
||||||
import android.view.autofill.AutofillValue
|
import android.view.autofill.AutofillValue
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.github.ajalt.timberkt.e
|
import com.github.ajalt.timberkt.e
|
||||||
import com.github.michaelbull.result.getOrElse
|
|
||||||
import com.github.michaelbull.result.runCatching
|
|
||||||
|
|
||||||
enum class AutofillAction {
|
enum class AutofillAction {
|
||||||
Match, Search, Generate, FillOtpFromSms
|
Match, Search, Generate, FillOtpFromSms
|
||||||
|
@ -38,7 +36,7 @@ sealed class AutofillScenario<out T : Any> {
|
||||||
const val BUNDLE_KEY_GENERIC_PASSWORD_IDS = "genericPasswordIds"
|
const val BUNDLE_KEY_GENERIC_PASSWORD_IDS = "genericPasswordIds"
|
||||||
|
|
||||||
fun fromBundle(clientState: Bundle): AutofillScenario<AutofillId>? {
|
fun fromBundle(clientState: Bundle): AutofillScenario<AutofillId>? {
|
||||||
return runCatching {
|
return try {
|
||||||
Builder<AutofillId>().apply {
|
Builder<AutofillId>().apply {
|
||||||
username = clientState.getParcelable(BUNDLE_KEY_USERNAME_ID)
|
username = clientState.getParcelable(BUNDLE_KEY_USERNAME_ID)
|
||||||
fillUsername = clientState.getBoolean(BUNDLE_KEY_FILL_USERNAME)
|
fillUsername = clientState.getBoolean(BUNDLE_KEY_FILL_USERNAME)
|
||||||
|
@ -59,7 +57,7 @@ sealed class AutofillScenario<out T : Any> {
|
||||||
) ?: emptyList()
|
) ?: emptyList()
|
||||||
)
|
)
|
||||||
}.build()
|
}.build()
|
||||||
}.getOrElse { e ->
|
} catch(e: Throwable) {
|
||||||
e(e)
|
e(e)
|
||||||
null
|
null
|
||||||
}
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
* 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 android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import com.zeapo.pwdstore.autofill.oreo.CertaintyLevel.Certain
|
import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Certain
|
||||||
import com.zeapo.pwdstore.autofill.oreo.CertaintyLevel.Likely
|
import com.github.androidpasswordstore.autofillparser.CertaintyLevel.Likely
|
||||||
|
|
||||||
private inline fun <T> Pair<T, T>.all(predicate: T.() -> Boolean) =
|
private inline fun <T> Pair<T, T>.all(predicate: T.() -> Boolean) =
|
||||||
predicate(first) && predicate(second)
|
predicate(first) && predicate(second)
|
|
@ -1,8 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
* 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 android.os.Build
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
|
@ -1,8 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
* 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.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
|
@ -1,8 +1,8 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
* 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.app.assist.AssistStructure
|
||||||
import android.os.Build
|
import android.os.Build
|
|
@ -1,14 +1,11 @@
|
||||||
/*
|
/*
|
||||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
* 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.Context
|
||||||
import android.util.Patterns
|
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 kotlinx.coroutines.runBlocking
|
||||||
import mozilla.components.lib.publicsuffixlist.PublicSuffixList
|
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
|
* Note: Invalid domains, such as IP addresses, are returned unchanged and thus never collide with
|
||||||
* the return value for valid domains.
|
* 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 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
|
// 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,
|
// exists). As long as we restrict ourselves to syntactically valid domain names,
|
||||||
|
@ -48,7 +45,7 @@ fun getPublicSuffixPlusOne(context: Context, domain: String) = runBlocking {
|
||||||
) {
|
) {
|
||||||
domain
|
domain
|
||||||
} else {
|
} else {
|
||||||
getCanonicalSuffix(context, domain)
|
getCanonicalSuffix(context, domain, customSuffixes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,19 +65,12 @@ fun getSuffixPlusUpToOne(domain: String, suffix: String): String? {
|
||||||
return "$lastPrefixPart.$suffix"
|
return "$lastPrefixPart.$suffix"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCustomSuffixes(context: Context): Sequence<String> {
|
suspend fun getCanonicalSuffix(context: Context, domain: String, customSuffixes: Sequence<String>): 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 {
|
|
||||||
val publicSuffixList = PublicSuffixListCache.getOrCachePublicSuffixList(context)
|
val publicSuffixList = PublicSuffixListCache.getOrCachePublicSuffixList(context)
|
||||||
val publicSuffixPlusOne = publicSuffixList.getPublicSuffixPlusOne(domain).await()
|
val publicSuffixPlusOne = publicSuffixList.getPublicSuffixPlusOne(domain).await()
|
||||||
?: return domain
|
?: return domain
|
||||||
var longestSuffix = publicSuffixPlusOne
|
var longestSuffix = publicSuffixPlusOne
|
||||||
for (customSuffix in getCustomSuffixes(context)) {
|
for (customSuffix in customSuffixes) {
|
||||||
val suffixPlusUpToOne = getSuffixPlusUpToOne(domain, customSuffix) ?: continue
|
val suffixPlusUpToOne = getSuffixPlusUpToOne(domain, customSuffix) ?: continue
|
||||||
// A shorter suffix is automatically a substring.
|
// A shorter suffix is automatically a substring.
|
||||||
if (suffixPlusUpToOne.length > longestSuffix.length)
|
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
|
/* 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
|
* 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
|
/* 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
|
* 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
|
/* 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
|
* 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
|
/* 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
|
* 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 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 zxing_android_embedded = "com.github.android-password-store:zxing-android-embedded:4.1.0-aps"
|
||||||
|
const val autofill_parser = ":autofill-parser"
|
||||||
}
|
}
|
||||||
|
|
||||||
object ThirdParty {
|
object ThirdParty {
|
||||||
|
|
|
@ -2,4 +2,5 @@
|
||||||
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
|
||||||
* SPDX-License-Identifier: GPL-3.0-only
|
* SPDX-License-Identifier: GPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
include(":autofill-parser")
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|
Loading…
Reference in a new issue