diff options
| author | Harsh Shandilya <me@msfjarvis.dev> | 2020-12-05 06:07:18 +0530 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-12-05 06:07:18 +0530 |
| commit | 5e66d99c852ea67a88b650c03b0e8d55e83eccde (patch) | |
| tree | aee323a08253fe7b863976eff92a1bd412bd1430 /app/src/main/java/com/zeapo/pwdstore/autofill | |
| parent | 8eb55f18a11d6b2f155c7c9d48ca833313ee13db (diff) | |
| download | HardenedPass-5e66d99c852ea67a88b650c03b0e8d55e83eccde.tar.gz HardenedPass-5e66d99c852ea67a88b650c03b0e8d55e83eccde.zip | |
Refactor package structure (#1233)
* idea: default test runner to Gradle
* Kick off package structure revamp
* Reparent all classes under dev.msfjarvis.aps
Signed-off-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/autofill')
11 files changed, 0 insertions, 1723 deletions
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Api30AutofillResponseBuilder.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Api30AutofillResponseBuilder.kt deleted file mode 100644 index a69e51a7..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/Api30AutofillResponseBuilder.kt +++ /dev/null @@ -1,189 +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.content.Context -import android.content.IntentSender -import android.os.Build -import android.service.autofill.Dataset -import android.service.autofill.FillCallback -import android.service.autofill.FillResponse -import android.service.autofill.SaveInfo -import android.view.inputmethod.InlineSuggestionsRequest -import android.widget.inline.InlinePresentationSpec -import androidx.annotation.RequiresApi -import com.github.ajalt.timberkt.e -import com.github.androidpasswordstore.autofillparser.AutofillAction -import com.github.androidpasswordstore.autofillparser.FillableForm -import com.github.androidpasswordstore.autofillparser.fillWith -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 - -/** - * Implements [AutofillResponseBuilder]'s methods for API 30 and above - */ -@RequiresApi(Build.VERSION_CODES.R) -class Api30AutofillResponseBuilder(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.hasPasswordFieldsToSave - private val canBeSaved = saveFlags != null && scenarioSupportsSave - - private fun makeIntentDataset( - context: Context, - action: AutofillAction, - intentSender: IntentSender, - metadata: DatasetMetadata, - imeSpec: InlinePresentationSpec?, - ): Dataset { - return Dataset.Builder(makeRemoteView(context, metadata)).run { - fillWith(scenario, action, credentials = null) - setAuthentication(intentSender) - if (imeSpec != null) { - val inlinePresentation = makeInlinePresentation(context, imeSpec, metadata) - if (inlinePresentation != null) { - setInlinePresentation(inlinePresentation) - } - } - build() - } - } - - private fun makeMatchDataset(context: Context, file: File, imeSpec: InlinePresentationSpec?): Dataset? { - if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null - val metadata = makeFillMatchMetadata(context, file) - val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context) - return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec) - } - - private fun makeSearchDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? { - if (!scenario.hasFieldsToFillOn(AutofillAction.Search)) return null - val metadata = makeSearchAndFillMetadata(context) - val intentSender = - AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin) - return makeIntentDataset(context, AutofillAction.Search, intentSender, metadata, imeSpec) - } - - private fun makeGenerateDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? { - if (!scenario.hasFieldsToFillOn(AutofillAction.Generate)) return null - val metadata = makeGenerateAndFillMetadata(context) - val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin) - return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata, imeSpec) - } - - - private fun makeFillOtpFromSmsDataset(context: Context, imeSpec: InlinePresentationSpec?): Dataset? { - if (!scenario.hasFieldsToFillOn(AutofillAction.FillOtpFromSms)) return null - if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null - val metadata = makeFillOtpFromSmsMetadata(context) - val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context) - return makeIntentDataset(context, AutofillAction.FillOtpFromSms, intentSender, metadata, imeSpec) - } - - private fun makePublisherChangedDataset( - context: Context, - publisherChangedException: AutofillPublisherChangedException, - imeSpec: InlinePresentationSpec? - ): Dataset { - val metadata = makeWarningMetadata(context) - // If the user decides to trust the new publisher, they can choose reset the list of - // matches. In this case we need to immediately show a new `FillResponse` as if the app were - // autofilled for the first time. This `FillResponse` needs to be returned as a result from - // `AutofillPublisherChangedActivity`, which is why we create and pass it on here. - val fillResponseAfterReset = makeFillResponse(context, null, emptyList()) - val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender( - context, publisherChangedException, fillResponseAfterReset - ) - return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata, imeSpec) - } - - private fun makePublisherChangedResponse( - context: Context, - inlineSuggestionsRequest: InlineSuggestionsRequest?, - publisherChangedException: AutofillPublisherChangedException - ): FillResponse { - val imeSpec = inlineSuggestionsRequest?.inlinePresentationSpecs?.firstOrNull() - return FillResponse.Builder().run { - addDataset(makePublisherChangedDataset(context, publisherChangedException, imeSpec)) - setIgnoredIds(*ignoredIds.toTypedArray()) - build() - } - } - - private fun makeFillResponse(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, matchedFiles: List<File>): FillResponse? { - var datasetCount = 0 - val imeSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs ?: emptyList() - return FillResponse.Builder().run { - for (file in matchedFiles) { - makeMatchDataset(context, file, imeSpecs.getOrNull(datasetCount))?.let { - datasetCount++ - addDataset(it) - } - } - makeSearchDataset(context, imeSpecs.getOrNull(datasetCount))?.let { - datasetCount++ - addDataset(it) - } - makeGenerateDataset(context, imeSpecs.getOrNull(datasetCount))?.let { - datasetCount++ - addDataset(it) - } - makeFillOtpFromSmsDataset(context, imeSpecs.getOrNull(datasetCount))?.let { - datasetCount++ - addDataset(it) - } - if (datasetCount == 0) return null - setHeader(makeRemoteView(context, makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true)))) - makeSaveInfo()?.let { setSaveInfo(it) } - setClientState(clientState) - 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.toTypedArray() - if (idsToSave.isEmpty()) return null - var saveDataTypes = SaveInfo.SAVE_DATA_TYPE_PASSWORD - if (scenario.hasUsername) { - saveDataTypes = saveDataTypes or SaveInfo.SAVE_DATA_TYPE_USERNAME - } - return SaveInfo.Builder(saveDataTypes, idsToSave).run { - setFlags(saveFlags) - build() - } - } - - /** - * Creates and returns a suitable [FillResponse] to the Autofill framework. - */ - fun fillCredentials(context: Context, inlineSuggestionsRequest: InlineSuggestionsRequest?, callback: FillCallback) { - AutofillMatcher.getMatchesFor(context, formOrigin).fold( - success = { matchedFiles -> - callback.onSuccess(makeFillResponse(context, inlineSuggestionsRequest, matchedFiles)) - }, - failure = { e -> - e(e) - callback.onSuccess(makePublisherChangedResponse(context, inlineSuggestionsRequest, e)) - } - ) - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillMatcher.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillMatcher.kt deleted file mode 100644 index 601be34a..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillMatcher.kt +++ /dev/null @@ -1,193 +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.content.Context -import android.content.SharedPreferences -import android.widget.Toast -import androidx.core.content.edit -import com.github.ajalt.timberkt.Timber.e -import com.github.ajalt.timberkt.d -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 - -private const val PREFERENCES_AUTOFILL_APP_MATCHES = "oreo_autofill_app_matches" -private val Context.autofillAppMatches - get() = getSharedPreferences(PREFERENCES_AUTOFILL_APP_MATCHES, Context.MODE_PRIVATE) - -private const val PREFERENCES_AUTOFILL_WEB_MATCHES = "oreo_autofill_web_matches" -private val Context.autofillWebMatches - get() = getSharedPreferences(PREFERENCES_AUTOFILL_WEB_MATCHES, Context.MODE_PRIVATE) - -private fun Context.matchPreferences(formOrigin: FormOrigin): SharedPreferences { - return when (formOrigin) { - is FormOrigin.App -> autofillAppMatches - is FormOrigin.Web -> autofillWebMatches - } -} - -class AutofillPublisherChangedException(val formOrigin: FormOrigin) : - Exception("The publisher of '${formOrigin.identifier}' changed since an entry was first matched with this app") { - - init { - require(formOrigin is FormOrigin.App) - } -} - -/** - * Manages "matches", i.e., associations between apps or websites and Password Store entries. - */ -class AutofillMatcher { - - companion object { - - private const val MAX_NUM_MATCHES = 10 - - private const val PREFERENCE_PREFIX_TOKEN = "token;" - private fun tokenKey(formOrigin: FormOrigin.App) = - "$PREFERENCE_PREFIX_TOKEN${formOrigin.identifier}" - - private const val PREFERENCE_PREFIX_MATCHES = "matches;" - private fun matchesKey(formOrigin: FormOrigin) = - "$PREFERENCE_PREFIX_MATCHES${formOrigin.identifier}" - - private fun hasFormOriginHashChanged(context: Context, formOrigin: FormOrigin): Boolean { - return when (formOrigin) { - is FormOrigin.Web -> false - is FormOrigin.App -> { - val packageName = formOrigin.identifier - val certificatesHash = computeCertificatesHash(context, packageName) - val storedCertificatesHash = - context.autofillAppMatches.getString(tokenKey(formOrigin), null) - ?: return false - val hashHasChanged = certificatesHash != storedCertificatesHash - if (hashHasChanged) { - e { "$packageName: stored=$storedCertificatesHash, new=$certificatesHash" } - true - } else { - false - } - } - } - } - - private fun storeFormOriginHash(context: Context, formOrigin: FormOrigin) { - if (formOrigin is FormOrigin.App) { - val packageName = formOrigin.identifier - val certificatesHash = computeCertificatesHash(context, packageName) - context.autofillAppMatches.edit { - putString(tokenKey(formOrigin), certificatesHash) - } - } - // We don't need to store a hash for FormOrigin.Web since it can only originate from - // browsers we trust to verify the origin. - } - - /** - * Get all Password Store entries that have already been associated with [formOrigin] by the - * user. - * - * If [formOrigin] represents an app and that app's certificates have changed since the - * first time the user associated an entry with it, an [AutofillPublisherChangedException] - * will be thrown. - */ - fun getMatchesFor(context: Context, formOrigin: FormOrigin): Result<List<File>, AutofillPublisherChangedException> { - if (hasFormOriginHashChanged(context, formOrigin)) { - return Err(AutofillPublisherChangedException(formOrigin)) - } - val matchPreferences = context.matchPreferences(formOrigin) - val matchedFiles = - matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) } - return Ok(matchedFiles.filter { it.exists() }.also { validFiles -> - matchPreferences.edit { - putStringSet(matchesKey(formOrigin), validFiles.map { it.absolutePath }.toSet()) - } - }) - } - - fun clearMatchesFor(context: Context, formOrigin: FormOrigin) { - context.matchPreferences(formOrigin).edit { - remove(matchesKey(formOrigin)) - if (formOrigin is FormOrigin.App) remove(tokenKey(formOrigin)) - } - } - - /** - * Associates the store entry [file] with [formOrigin], such that future Autofill responses - * to requests from this app or website offer this entry as a dataset. - * - * The maximum number of matches is limited by [MAX_NUM_MATCHES] since older versions of - * Android may crash when too many datasets are offered. - */ - fun addMatchFor(context: Context, formOrigin: FormOrigin, file: File) { - if (!file.exists()) return - if (hasFormOriginHashChanged(context, formOrigin)) { - // This should never happen since we already verified the publisher in - // getMatchesFor. - e { "App publisher changed between getMatchesFor and addMatchFor" } - throw AutofillPublisherChangedException(formOrigin) - } - val matchPreferences = context.matchPreferences(formOrigin) - val matchedFiles = - matchPreferences.getStringSet(matchesKey(formOrigin), emptySet())!!.map { File(it) } - val newFiles = setOf(file.absoluteFile).union(matchedFiles) - if (newFiles.size > MAX_NUM_MATCHES) { - Toast.makeText( - context, - context.getString(R.string.oreo_autofill_max_matches_reached, MAX_NUM_MATCHES), - Toast.LENGTH_LONG - ).show() - return - } - matchPreferences.edit { - putStringSet(matchesKey(formOrigin), newFiles.map { it.absolutePath }.toSet()) - } - storeFormOriginHash(context, formOrigin) - d { "Stored match for $formOrigin" } - } - - /** - * Goes through all existing matches and updates their associated entries by using - * [moveFromTo] as a lookup table and deleting the matches for files in [delete]. - */ - fun updateMatches(context: Context, moveFromTo: Map<File, File> = emptyMap(), delete: Collection<File> = emptyList()) { - val deletePathList = delete.map { it.absolutePath } - val oldNewPathMap = moveFromTo.mapValues { it.value.absolutePath } - .mapKeys { it.key.absolutePath } - for (prefs in listOf(context.autofillAppMatches, context.autofillWebMatches)) { - for ((key, value) in prefs.all) { - if (!key.startsWith(PREFERENCE_PREFIX_MATCHES)) continue - // We know that preferences starting with `PREFERENCE_PREFIX_MATCHES` were - // created with `putStringSet`. - @Suppress("UNCHECKED_CAST") - val oldMatches = value as? Set<String> - if (oldMatches == null) { - w { "Failed to read matches for $key" } - continue - } - // Delete all matches for file locations that are going to be overwritten, then - // transfer matches over to the files at their new locations. - val newMatches = - oldMatches.asSequence() - .minus(deletePathList) - .minus(oldNewPathMap.values) - .map { match -> - val newPath = oldNewPathMap[match] ?: return@map match - d { "Updating match for $key: $match --> $newPath" } - newPath - }.toSet() - if (newMatches != oldMatches) - prefs.edit { putStringSet(key, newMatches) } - } - } - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt deleted file mode 100644 index cc0875f3..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillPreferences.kt +++ /dev/null @@ -1,139 +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.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 - -enum class DirectoryStructure(val value: String) { - EncryptedUsername("encrypted_username"), - FileBased("file"), - DirectoryBased("directory"); - - /** - * Returns the username associated to [file], following the convention of the current - * [DirectoryStructure]. - * - * Examples: - * - * --> null (EncryptedUsername) - * - work/example.org/john@doe.org.gpg --> john@doe.org (FileBased) - * - work/example.org/john@doe.org/password.gpg --> john@doe.org (DirectoryBased) - * - Temporary PIN.gpg --> Temporary PIN (DirectoryBased, fallback) - */ - fun getUsernameFor(file: File): String? = when (this) { - EncryptedUsername -> null - FileBased -> file.nameWithoutExtension - DirectoryBased -> file.parentFile?.name ?: file.nameWithoutExtension - } - - /** - * Returns the origin identifier associated to [file], following the convention of the current - * [DirectoryStructure]. - * - * At least one of [DirectoryStructure.getIdentifierFor] and - * [DirectoryStructure.getAccountPartFor] will always return a non-null result. - * - * Examples: - * - work/example.org.gpg --> example.org (EncryptedUsername) - * - work/example.org/john@doe.org.gpg --> example.org (FileBased) - * - example.org.gpg --> example.org (FileBased, fallback) - * - work/example.org/john@doe.org/password.gpg --> example.org (DirectoryBased) - * - Temporary PIN.gpg --> null (DirectoryBased) - */ - fun getIdentifierFor(file: File): String? = when (this) { - EncryptedUsername -> file.nameWithoutExtension - FileBased -> file.parentFile?.name ?: file.nameWithoutExtension - DirectoryBased -> file.parentFile?.parent - } - - /** - * Returns the path components of [file] until right before the component that contains the - * origin identifier according to the current [DirectoryStructure]. - * - * Examples: - * - work/example.org.gpg --> work (EncryptedUsername) - * - work/example.org/john@doe.org.gpg --> work (FileBased) - * - example.org/john@doe.org.gpg --> null (FileBased) - * - john@doe.org.gpg --> null (FileBased) - * - work/example.org/john@doe.org/password.gpg --> work (DirectoryBased) - * - example.org/john@doe.org/password.gpg --> null (DirectoryBased) - */ - fun getPathToIdentifierFor(file: File): String? = when (this) { - EncryptedUsername -> file.parent - FileBased -> file.parentFile?.parent - DirectoryBased -> file.parentFile?.parentFile?.parent - } - - /** - * Returns the path component of [file] following the origin identifier according to the current - * [DirectoryStructure] (without file extension). - * - * At least one of [DirectoryStructure.getIdentifierFor] and - * [DirectoryStructure.getAccountPartFor] will always return a non-null result. - * - * Examples: - * - * --> null (EncryptedUsername) - * - work/example.org/john@doe.org.gpg --> john@doe.org (FileBased) - * - example.org.gpg --> null (FileBased, fallback) - * - work/example.org/john@doe.org/password.gpg --> john@doe.org/password (DirectoryBased) - * - Temporary PIN.gpg --> Temporary PIN (DirectoryBased, fallback) - */ - fun getAccountPartFor(file: File): String? = when (this) { - EncryptedUsername -> null - FileBased -> file.nameWithoutExtension.takeIf { file.parentFile != null } - DirectoryBased -> file.parentFile?.let { parentFile -> - "${parentFile.name}/${file.nameWithoutExtension}" - } ?: file.nameWithoutExtension - } - - @RequiresApi(Build.VERSION_CODES.O) - fun getSaveFolderName(sanitizedIdentifier: String, username: String?) = when (this) { - EncryptedUsername -> "/" - FileBased -> sanitizedIdentifier - DirectoryBased -> Paths.get(sanitizedIdentifier, username ?: "username").toString() - } - - fun getSaveFileName(username: String?, identifier: String) = when (this) { - EncryptedUsername -> identifier - FileBased -> username - DirectoryBased -> "password" - } - - companion object { - - const val PREFERENCE = "oreo_autofill_directory_structure" - private val DEFAULT = FileBased - - private val reverseMap = values().associateBy { it.value } - fun fromValue(value: String?) = if (value != null) reverseMap[value] ?: DEFAULT else DEFAULT - } -} - -object AutofillPreferences { - - fun directoryStructure(context: Context): DirectoryStructure { - val value = context.sharedPrefs.getString(DirectoryStructure.PREFERENCE, null) - return DirectoryStructure.fromValue(value) - } - - fun credentialsFromStoreEntry( - context: Context, - file: File, - entry: PasswordEntry, - directoryStructure: DirectoryStructure - ): Credentials { - // Always give priority to a username stored in the encrypted extras - val username = entry.username - ?: directoryStructure.getUsernameFor(file) - ?: context.getDefaultUsername() - return Credentials(username, entry.password, entry.calculateTotpCode()) - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt deleted file mode 100644 index d73373e3..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillResponseBuilder.kt +++ /dev/null @@ -1,203 +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.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 androidx.annotation.RequiresApi -import com.github.ajalt.timberkt.e -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.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 - -@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.hasPasswordFieldsToSave - private val canBeSaved = saveFlags != null && scenarioSupportsSave - - private fun makeIntentDataset( - context: Context, - action: AutofillAction, - intentSender: IntentSender, - metadata: DatasetMetadata, - ): Dataset { - return Dataset.Builder(makeRemoteView(context, metadata)).run { - fillWith(scenario, action, credentials = null) - setAuthentication(intentSender) - build() - } - } - - private fun makeMatchDataset(context: Context, file: File): Dataset? { - if (!scenario.hasFieldsToFillOn(AutofillAction.Match)) return null - val metadata = makeFillMatchMetadata(context, file) - val intentSender = AutofillDecryptActivity.makeDecryptFileIntentSender(file, context) - return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata) - } - - - private fun makeSearchDataset(context: Context): Dataset? { - if (!scenario.hasFieldsToFillOn(AutofillAction.Search)) return null - val metadata = makeSearchAndFillMetadata(context) - val intentSender = - AutofillFilterView.makeMatchAndDecryptFileIntentSender(context, formOrigin) - return makeIntentDataset(context, AutofillAction.Search, intentSender, metadata) - } - - private fun makeGenerateDataset(context: Context): Dataset? { - if (!scenario.hasFieldsToFillOn(AutofillAction.Generate)) return null - val metadata = makeGenerateAndFillMetadata(context) - val intentSender = AutofillSaveActivity.makeSaveIntentSender(context, null, formOrigin) - return makeIntentDataset(context, AutofillAction.Generate, intentSender, metadata) - } - - private fun makeFillOtpFromSmsDataset(context: Context): Dataset? { - if (!scenario.hasFieldsToFillOn(AutofillAction.FillOtpFromSms)) return null - if (!AutofillSmsActivity.shouldOfferFillFromSms(context)) return null - val metadata = makeFillOtpFromSmsMetadata(context) - val intentSender = AutofillSmsActivity.makeFillOtpFromSmsIntentSender(context) - return makeIntentDataset(context, AutofillAction.FillOtpFromSms, intentSender, metadata) - } - - private fun makePublisherChangedDataset( - context: Context, - publisherChangedException: AutofillPublisherChangedException, - ): Dataset { - val metadata = makeWarningMetadata(context) - // If the user decides to trust the new publisher, they can choose reset the list of - // matches. In this case we need to immediately show a new `FillResponse` as if the app were - // autofilled for the first time. This `FillResponse` needs to be returned as a result from - // `AutofillPublisherChangedActivity`, which is why we create and pass it on here. - val fillResponseAfterReset = makeFillResponse(context, emptyList()) - val intentSender = AutofillPublisherChangedActivity.makePublisherChangedIntentSender( - context, publisherChangedException, fillResponseAfterReset - ) - return makeIntentDataset(context, AutofillAction.Match, intentSender, metadata) - } - - 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.toTypedArray() - if (idsToSave.isEmpty()) return null - var saveDataTypes = SaveInfo.SAVE_DATA_TYPE_PASSWORD - if (scenario.hasUsername) { - 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 datasetCount = 0 - return FillResponse.Builder().run { - for (file in matchedFiles) { - makeMatchDataset(context, file)?.let { - datasetCount++ - addDataset(it) - } - } - makeSearchDataset(context)?.let { - datasetCount++ - addDataset(it) - } - makeGenerateDataset(context)?.let { - datasetCount++ - addDataset(it) - } - makeFillOtpFromSmsDataset(context)?.let { - datasetCount++ - addDataset(it) - } - if (datasetCount == 0) return null - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - setHeader(makeRemoteView(context, makeHeaderMetadata(formOrigin.getPrettyIdentifier(context, untrusted = true)))) - } - 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 scenario = AutofillScenario.fromClientState(clientState) - // Before Android P, Datasets used for fill-in had to come with a RemoteViews, even - // though they are rarely shown. - // FIXME: We should clone the original dataset here and add the credentials to be filled - // in. Otherwise, the entry in the cached list of datasets will be overwritten by the - // fill-in dataset without any visual representation. This causes it to be missing from - // the Autofill suggestions shown after the user clears the filled out form fields. - val builder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - Dataset.Builder() - } else { - Dataset.Builder(makeRemoteView(context, makeEmptyMetadata())) - } - return builder.run { - if (scenario != null) fillWith(scenario, action, credentials) - else e { "Failed to recover scenario from client state" } - build() - } - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillViewUtils.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillViewUtils.kt deleted file mode 100644 index 49e0d3e3..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/AutofillViewUtils.kt +++ /dev/null @@ -1,113 +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.annotation.SuppressLint -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.res.Resources -import android.graphics.drawable.Icon -import android.os.Build -import android.service.autofill.InlinePresentation -import android.view.View -import android.widget.RemoteViews -import android.widget.inline.InlinePresentationSpec -import androidx.annotation.DrawableRes -import androidx.autofill.inline.UiVersions -import androidx.autofill.inline.v1.InlineSuggestionUi -import com.zeapo.pwdstore.PasswordStore -import com.zeapo.pwdstore.R -import com.zeapo.pwdstore.utils.PasswordRepository -import java.io.File - -data class DatasetMetadata(val title: String, val subtitle: String?, @DrawableRes val iconRes: Int) - -fun makeRemoteView(context: Context, metadata: DatasetMetadata): RemoteViews { - return RemoteViews(context.packageName, R.layout.oreo_autofill_dataset).apply { - setTextViewText(R.id.title, metadata.title) - if (metadata.subtitle != null) { - setTextViewText(R.id.summary, metadata.subtitle) - } else { - setViewVisibility(R.id.summary, View.GONE) - } - if (metadata.iconRes != Resources.ID_NULL) { - setImageViewResource(R.id.icon, metadata.iconRes) - } else { - setViewVisibility(R.id.icon, View.GONE) - } - } -} - -@SuppressLint("RestrictedApi") -fun makeInlinePresentation(context: Context, imeSpec: InlinePresentationSpec, metadata: DatasetMetadata): InlinePresentation? { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) - return null - - if (UiVersions.INLINE_UI_VERSION_1 !in UiVersions.getVersions(imeSpec.style)) - return null - - val launchIntent = PendingIntent.getActivity(context, 0, Intent(context, PasswordStore::class.java), 0) - val slice = InlineSuggestionUi.newContentBuilder(launchIntent).run { - setTitle(metadata.title) - if (metadata.subtitle != null) - setSubtitle(metadata.subtitle) - setContentDescription(if (metadata.subtitle != null) "${metadata.title} - ${metadata.subtitle}" else metadata.title) - setStartIcon(Icon.createWithResource(context, metadata.iconRes)) - build().slice - } - - return InlinePresentation(slice, imeSpec, false) -} - - -fun makeFillMatchMetadata(context: Context, file: File): DatasetMetadata { - val directoryStructure = AutofillPreferences.directoryStructure(context) - val relativeFile = file.relativeTo(PasswordRepository.getRepositoryDirectory()) - val title = directoryStructure.getIdentifierFor(relativeFile) - ?: directoryStructure.getAccountPartFor(relativeFile)!! - val subtitle = directoryStructure.getAccountPartFor(relativeFile) - return DatasetMetadata( - title, - subtitle, - R.drawable.ic_person_black_24dp - ) -} - -fun makeSearchAndFillMetadata(context: Context) = DatasetMetadata( - context.getString(R.string.oreo_autofill_search_in_store), - null, - R.drawable.ic_search_black_24dp -) - -fun makeGenerateAndFillMetadata(context: Context) = DatasetMetadata( - context.getString(R.string.oreo_autofill_generate_password), - null, - R.drawable.ic_autofill_new_password -) - -fun makeFillOtpFromSmsMetadata(context: Context) = DatasetMetadata( - context.getString(R.string.oreo_autofill_fill_otp_from_sms), - null, - R.drawable.ic_autofill_sms -) - -fun makeEmptyMetadata() = DatasetMetadata( - "PLACEHOLDER", - "PLACEHOLDER", - R.mipmap.ic_launcher -) - -fun makeWarningMetadata(context: Context) = DatasetMetadata( - context.getString(R.string.oreo_autofill_warning_publisher_dataset_title), - context.getString(R.string.oreo_autofill_warning_publisher_dataset_summary), - R.drawable.ic_warning_red_24dp -) - -fun makeHeaderMetadata(title: String) = DatasetMetadata( - title, - null, - 0 -) diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt deleted file mode 100644 index cc7e2c0c..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/OreoAutofillService.kt +++ /dev/null @@ -1,142 +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.content.Context -import android.os.Build -import android.os.CancellationSignal -import android.service.autofill.AutofillService -import android.service.autofill.FillCallback -import android.service.autofill.FillRequest -import android.service.autofill.FillResponse -import android.service.autofill.SaveCallback -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() { - - companion object { - - // TODO: Provide a user-configurable denylist - private val DENYLISTED_PACKAGES = listOf( - BuildConfig.APPLICATION_ID, - "android", - "com.android.settings", - "com.android.settings.intelligence", - "com.android.systemui", - "com.oneplus.applocker", - "org.sufficientlysecure.keychain", - ) - - private const val DISABLE_AUTOFILL_DURATION_MS = 1000 * 60 * 60 * 24L - } - - override fun onCreate() { - super.onCreate() - cachePublicSuffixList(applicationContext) - } - - override fun onFillRequest( - request: FillRequest, - cancellationSignal: CancellationSignal, - callback: FillCallback - ) { - val structure = request.fillContexts.lastOrNull()?.structure ?: run { - callback.onSuccess(null) - return - } - if (structure.activityComponent.packageName in DENYLISTED_PACKAGES) { - if (Build.VERSION.SDK_INT >= 28) { - callback.onSuccess(FillResponse.Builder().run { - disableAutofill(DISABLE_AUTOFILL_DURATION_MS) - build() - }) - } else { - callback.onSuccess(null) - } - return - } - val formToFill = FillableForm.parseAssistStructure( - this, structure, - isManualRequest = request.flags hasFlag FillRequest.FLAG_MANUAL_REQUEST, - getCustomSuffixes(), - ) ?: run { - d { "Form cannot be filled" } - callback.onSuccess(null) - return - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - Api30AutofillResponseBuilder(formToFill).fillCredentials(this, request.inlineSuggestionsRequest, callback) - } else { - AutofillResponseBuilder(formToFill).fillCredentials(this, callback) - } - } - - override fun onSaveRequest(request: SaveRequest, callback: SaveCallback) { - // SaveCallback's behavior and feature set differs based on both target and device SDK, so - // we replace it with a wrapper that works the same in all situations. - @Suppress("NAME_SHADOWING") val callback = FixedSaveCallback(this, callback) - val structure = request.fillContexts.lastOrNull()?.structure ?: run { - callback.onFailure(getString(R.string.oreo_autofill_save_app_not_supported)) - return - } - val clientState = request.clientState ?: run { - e { "Received save request without client state" } - callback.onFailure(getString(R.string.oreo_autofill_save_internal_error)) - return - } - val scenario = AutofillScenario.fromClientState(clientState)?.recoverNodes(structure) ?: run { - e { "Failed to recover client state or nodes from client state" } - callback.onFailure(getString(R.string.oreo_autofill_save_internal_error)) - return - } - val formOrigin = FormOrigin.fromBundle(clientState) ?: run { - e { "Failed to recover form origin from client state" } - callback.onFailure(getString(R.string.oreo_autofill_save_internal_error)) - return - } - - val username = scenario.usernameValue - val password = scenario.passwordValue ?: run { - callback.onFailure(getString(R.string.oreo_autofill_save_passwords_dont_match)) - return - } - callback.onSuccess( - AutofillSaveActivity.makeSaveIntentSender( - this, - credentials = Credentials(username, password, null), - formOrigin = formOrigin - ) - ) - } -} - -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() -} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt deleted file mode 100644 index a5bdcfe0..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillDecryptActivity.kt +++ /dev/null @@ -1,245 +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.ui - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.IntentSender -import android.os.Build -import android.os.Bundle -import android.view.autofill.AutofillManager -import android.widget.Toast -import androidx.activity.result.IntentSenderRequest -import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult -import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatActivity -import com.github.ajalt.timberkt.d -import com.github.ajalt.timberkt.e -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.github.androidpasswordstore.autofillparser.AutofillAction -import com.github.androidpasswordstore.autofillparser.Credentials -import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences -import com.zeapo.pwdstore.autofill.oreo.AutofillResponseBuilder -import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure -import com.zeapo.pwdstore.model.PasswordEntry -import com.zeapo.pwdstore.utils.OPENPGP_PROVIDER -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.InputStream -import java.io.OutputStream -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancelChildren -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import me.msfjarvis.openpgpktx.util.OpenPgpApi -import me.msfjarvis.openpgpktx.util.OpenPgpServiceConnection -import org.openintents.openpgp.IOpenPgpService2 -import org.openintents.openpgp.OpenPgpError - -@RequiresApi(Build.VERSION_CODES.O) -class AutofillDecryptActivity : AppCompatActivity(), CoroutineScope { - - companion object { - - private const val EXTRA_FILE_PATH = "com.zeapo.pwdstore.autofill.oreo.EXTRA_FILE_PATH" - private const val EXTRA_SEARCH_ACTION = - "com.zeapo.pwdstore.autofill.oreo.EXTRA_SEARCH_ACTION" - - private var decryptFileRequestCode = 1 - - fun makeDecryptFileIntent(file: File, forwardedExtras: Bundle, context: Context): Intent { - return Intent(context, AutofillDecryptActivity::class.java).apply { - putExtras(forwardedExtras) - putExtra(EXTRA_SEARCH_ACTION, true) - putExtra(EXTRA_FILE_PATH, file.absolutePath) - } - } - - fun makeDecryptFileIntentSender(file: File, context: Context): IntentSender { - val intent = Intent(context, AutofillDecryptActivity::class.java).apply { - putExtra(EXTRA_SEARCH_ACTION, false) - putExtra(EXTRA_FILE_PATH, file.absolutePath) - } - return PendingIntent.getActivity( - context, - decryptFileRequestCode++, - intent, - PendingIntent.FLAG_CANCEL_CURRENT - ).intentSender - } - } - - private val decryptInteractionRequiredAction = registerForActivityResult(StartIntentSenderForResult()) { result -> - if (continueAfterUserInteraction != null) { - val data = result.data - if (result.resultCode == RESULT_OK && data != null) { - continueAfterUserInteraction?.resume(data) - } else { - continueAfterUserInteraction?.resumeWithException(Exception("OpenPgpApi ACTION_DECRYPT_VERIFY failed to continue after user interaction")) - } - continueAfterUserInteraction = null - } - } - - private var continueAfterUserInteraction: Continuation<Intent>? = null - private lateinit var directoryStructure: DirectoryStructure - - override val coroutineContext - get() = Dispatchers.IO + SupervisorJob() - - override fun onStart() { - super.onStart() - val filePath = intent?.getStringExtra(EXTRA_FILE_PATH) ?: run { - e { "AutofillDecryptActivity started without EXTRA_FILE_PATH" } - finish() - return - } - val clientState = intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) ?: run { - e { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" } - finish() - return - } - val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!! - val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match - directoryStructure = AutofillPreferences.directoryStructure(this) - d { action.toString() } - launch { - val credentials = decryptCredential(File(filePath)) - if (credentials == null) { - setResult(RESULT_CANCELED) - } else { - val fillInDataset = - AutofillResponseBuilder.makeFillInDataset( - this@AutofillDecryptActivity, - credentials, - clientState, - action - ) - withContext(Dispatchers.Main) { - setResult(RESULT_OK, Intent().apply { - putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) - }) - } - } - withContext(Dispatchers.Main) { - finish() - } - } - } - - override fun onDestroy() { - super.onDestroy() - coroutineContext.cancelChildren() - } - - private suspend fun executeOpenPgpApi( - data: Intent, - input: InputStream, - output: OutputStream - ): Intent? { - var openPgpServiceConnection: OpenPgpServiceConnection? = null - val openPgpService = suspendCoroutine<IOpenPgpService2> { cont -> - openPgpServiceConnection = OpenPgpServiceConnection( - this, - OPENPGP_PROVIDER, - object : OpenPgpServiceConnection.OnBound { - override fun onBound(service: IOpenPgpService2) { - cont.resume(service) - } - - override fun onError(e: Exception) { - cont.resumeWithException(e) - } - }).also { it.bindToService() } - } - return OpenPgpApi(this, openPgpService).executeApi(data, input, output).also { - openPgpServiceConnection?.unbindFromService() - } - } - - private suspend fun decryptCredential( - file: File, - resumeIntent: Intent? = null - ): Credentials? { - val command = resumeIntent ?: Intent().apply { - action = OpenPgpApi.ACTION_DECRYPT_VERIFY - } - runCatching { - file.inputStream() - }.onFailure { e -> - e(e) { "File to decrypt not found" } - return null - }.onSuccess { encryptedInput -> - val decryptedOutput = ByteArrayOutputStream() - runCatching { - executeOpenPgpApi(command, encryptedInput, decryptedOutput) - }.onFailure { e -> - e(e) { "OpenPgpApi ACTION_DECRYPT_VERIFY failed" } - return null - }.onSuccess { result -> - return when (val resultCode = - result?.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) { - OpenPgpApi.RESULT_CODE_SUCCESS -> { - runCatching { - val entry = withContext(Dispatchers.IO) { - @Suppress("BlockingMethodInNonBlockingContext") - PasswordEntry(decryptedOutput) - } - AutofillPreferences.credentialsFromStoreEntry(this, file, entry, directoryStructure) - }.getOrElse { e -> - e(e) { "Failed to parse password entry" } - return null - } - } - OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> { - val pendingIntent: PendingIntent = - result.getParcelableExtra(OpenPgpApi.RESULT_INTENT)!! - runCatching { - val intentToResume = withContext(Dispatchers.Main) { - suspendCoroutine<Intent> { cont -> - continueAfterUserInteraction = cont - decryptInteractionRequiredAction.launch(IntentSenderRequest.Builder(pendingIntent.intentSender).build()) - } - } - decryptCredential(file, intentToResume) - }.getOrElse { e -> - e(e) { "OpenPgpApi ACTION_DECRYPT_VERIFY failed with user interaction" } - return null - } - } - OpenPgpApi.RESULT_CODE_ERROR -> { - val error = result.getParcelableExtra<OpenPgpError>(OpenPgpApi.RESULT_ERROR) - if (error != null) { - withContext(Dispatchers.Main) { - Toast.makeText( - applicationContext, - "Error from OpenKeyChain: ${error.message}", - Toast.LENGTH_LONG - ).show() - } - e { "OpenPgpApi ACTION_DECRYPT_VERIFY failed (${error.errorId}): ${error.message}" } - } - null - } - else -> { - e { "Unrecognized OpenPgpApi result: $resultCode" } - null - } - } - } - } - return null - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt deleted file mode 100644 index 25919ad5..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillFilterActivity.kt +++ /dev/null @@ -1,220 +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.ui - -import android.annotation.TargetApi -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.IntentSender -import android.os.Build -import android.os.Bundle -import android.view.View -import android.view.autofill.AutofillManager -import android.widget.TextView -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.core.text.bold -import androidx.core.text.buildSpannedString -import androidx.core.text.underline -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 -import com.zeapo.pwdstore.SearchMode -import com.zeapo.pwdstore.SearchableRepositoryAdapter -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.databinding.ActivityOreoAutofillFilterBinding -import com.zeapo.pwdstore.utils.PasswordItem -import com.zeapo.pwdstore.utils.viewBinding - -@TargetApi(Build.VERSION_CODES.O) -class AutofillFilterView : AppCompatActivity() { - - companion object { - - private const val HEIGHT_PERCENTAGE = 0.9 - private const val WIDTH_PERCENTAGE = 0.75 - - private const val EXTRA_FORM_ORIGIN_WEB = - "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_FORM_ORIGIN_WEB" - private const val EXTRA_FORM_ORIGIN_APP = - "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_FORM_ORIGIN_APP" - private var matchAndDecryptFileRequestCode = 1 - - fun makeMatchAndDecryptFileIntentSender( - context: Context, - formOrigin: FormOrigin - ): IntentSender { - val intent = Intent(context, AutofillFilterView::class.java).apply { - when (formOrigin) { - is FormOrigin.Web -> putExtra(EXTRA_FORM_ORIGIN_WEB, formOrigin.identifier) - is FormOrigin.App -> putExtra(EXTRA_FORM_ORIGIN_APP, formOrigin.identifier) - } - } - return PendingIntent.getActivity( - context, - matchAndDecryptFileRequestCode++, - intent, - PendingIntent.FLAG_CANCEL_CURRENT - ).intentSender - } - } - - private lateinit var formOrigin: FormOrigin - private lateinit var directoryStructure: DirectoryStructure - private val binding by viewBinding(ActivityOreoAutofillFilterBinding::inflate) - - private val model: SearchableRepositoryViewModel by viewModels { - ViewModelProvider.AndroidViewModelFactory(application) - } - - private val decryptAction = registerForActivityResult(StartActivityForResult()) { result -> - if (result.resultCode == RESULT_OK) { - setResult(RESULT_OK, result.data) - } - finish() - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - setFinishOnTouchOutside(true) - - val params = window.attributes - params.height = (HEIGHT_PERCENTAGE * resources.displayMetrics.heightPixels).toInt() - params.width = (WIDTH_PERCENTAGE * resources.displayMetrics.widthPixels).toInt() - window.attributes = params - - if (intent?.hasExtra(AutofillManager.EXTRA_CLIENT_STATE) != true) { - e { "AutofillFilterActivity started without EXTRA_CLIENT_STATE" } - finish() - return - } - formOrigin = when { - intent?.hasExtra(EXTRA_FORM_ORIGIN_WEB) == true -> { - FormOrigin.Web(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_WEB)!!) - } - intent?.hasExtra(EXTRA_FORM_ORIGIN_APP) == true -> { - FormOrigin.App(intent!!.getStringExtra(EXTRA_FORM_ORIGIN_APP)!!) - } - else -> { - e { "AutofillFilterActivity started without EXTRA_FORM_ORIGIN_WEB or EXTRA_FORM_ORIGIN_APP" } - finish() - return - } - } - directoryStructure = AutofillPreferences.directoryStructure(this) - - supportActionBar?.hide() - bindUI() - updateSearch() - setResult(RESULT_CANCELED) - } - - private fun bindUI() { - with(binding) { - rvPassword.apply { - adapter = SearchableRepositoryAdapter( - R.layout.oreo_autofill_filter_row, - ::PasswordViewHolder - ) { item -> - val file = item.file.relativeTo(item.rootDir) - val pathToIdentifier = directoryStructure.getPathToIdentifierFor(file) - val identifier = directoryStructure.getIdentifierFor(file) - val accountPart = directoryStructure.getAccountPartFor(file) - check(identifier != null || accountPart != null) { "At least one of identifier and accountPart should always be non-null" } - title.text = if (identifier != null) { - buildSpannedString { - if (pathToIdentifier != null) - append("$pathToIdentifier/") - bold { underline { append(identifier) } } - } - } else { - accountPart - } - subtitle.apply { - if (identifier != null && accountPart != null) { - text = accountPart - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } - }.onItemClicked { _, item -> - decryptAndFill(item) - } - layoutManager = LinearLayoutManager(context) - } - search.apply { - val initialSearch = - formOrigin.getPrettyIdentifier(applicationContext, untrusted = false) - setText(initialSearch, TextView.BufferType.EDITABLE) - addTextChangedListener { updateSearch() } - } - origin.text = buildSpannedString { - append(getString(R.string.oreo_autofill_select_and_fill_into)) - append("\n") - bold { - append(formOrigin.getPrettyIdentifier(applicationContext, untrusted = true)) - } - } - strictDomainSearch.apply { - visibility = if (formOrigin is FormOrigin.Web) View.VISIBLE else View.GONE - isChecked = formOrigin is FormOrigin.Web - setOnCheckedChangeListener { _, _ -> updateSearch() } - } - shouldMatch.text = getString( - R.string.oreo_autofill_match_with, - formOrigin.getPrettyIdentifier(applicationContext) - ) - model.searchResult.observe(this@AutofillFilterView) { result -> - val list = result.passwordItems - (rvPassword.adapter as SearchableRepositoryAdapter).submitList(list) { - rvPassword.scrollToPosition(0) - } - // Switch RecyclerView out for a "no results" message if the new list is empty and - // the message is not yet shown (and vice versa). - if ((list.isEmpty() && rvPasswordSwitcher.nextView.id == rvPasswordEmpty.id) || - (list.isNotEmpty() && rvPasswordSwitcher.nextView.id == rvPassword.id) - ) { - rvPasswordSwitcher.showNext() - } - } - } - } - - private fun updateSearch() { - model.search( - binding.search.text.toString().trim(), - filterMode = if (binding.strictDomainSearch.isChecked) FilterMode.StrictDomain else FilterMode.Fuzzy, - searchMode = SearchMode.RecursivelyInSubdirectories, - listMode = ListMode.FilesOnly - ) - } - - private fun decryptAndFill(item: PasswordItem) { - if (binding.shouldClear.isChecked) AutofillMatcher.clearMatchesFor(applicationContext, formOrigin) - if (binding.shouldMatch.isChecked) AutofillMatcher.addMatchFor( - applicationContext, - formOrigin, - item.file - ) - // intent?.extras? is checked to be non-null in onCreate - decryptAction.launch(AutofillDecryptActivity.makeDecryptFileIntent( - item.file, - intent!!.extras!!, - this - )) - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt deleted file mode 100644 index 34edf8f5..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillPublisherChangedActivity.kt +++ /dev/null @@ -1,114 +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.ui - -import android.annotation.TargetApi -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.IntentSender -import android.content.pm.PackageManager -import android.os.Build -import android.os.Bundle -import android.service.autofill.FillResponse -import android.text.format.DateUtils -import android.view.View -import android.view.autofill.AutofillManager -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.databinding.ActivityOreoAutofillPublisherChangedBinding -import com.zeapo.pwdstore.utils.viewBinding - -@TargetApi(Build.VERSION_CODES.O) -class AutofillPublisherChangedActivity : AppCompatActivity() { - - companion object { - - private const val EXTRA_APP_PACKAGE = - "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_APP_PACKAGE" - private const val EXTRA_FILL_RESPONSE_AFTER_RESET = - "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_FILL_RESPONSE_AFTER_RESET" - private var publisherChangedRequestCode = 1 - - fun makePublisherChangedIntentSender( - context: Context, - publisherChangedException: AutofillPublisherChangedException, - fillResponseAfterReset: FillResponse?, - ): IntentSender { - val intent = Intent(context, AutofillPublisherChangedActivity::class.java).apply { - putExtra(EXTRA_APP_PACKAGE, publisherChangedException.formOrigin.identifier) - putExtra(EXTRA_FILL_RESPONSE_AFTER_RESET, fillResponseAfterReset) - } - return PendingIntent.getActivity( - context, publisherChangedRequestCode++, intent, PendingIntent.FLAG_CANCEL_CURRENT - ).intentSender - } - } - - private lateinit var appPackage: String - private val binding by viewBinding(ActivityOreoAutofillPublisherChangedBinding::inflate) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(binding.root) - setFinishOnTouchOutside(true) - - appPackage = intent.getStringExtra(EXTRA_APP_PACKAGE) ?: run { - e { "AutofillPublisherChangedActivity started without EXTRA_PACKAGE_NAME" } - finish() - return - } - supportActionBar?.hide() - showPackageInfo() - with(binding) { - okButton.setOnClickListener { finish() } - advancedButton.setOnClickListener { - advancedButton.visibility = View.GONE - warningAppAdvancedInfo.visibility = View.VISIBLE - resetButton.visibility = View.VISIBLE - } - resetButton.setOnClickListener { - AutofillMatcher.clearMatchesFor(this@AutofillPublisherChangedActivity, FormOrigin.App(appPackage)) - val fillResponse = intent.getParcelableExtra<FillResponse>(EXTRA_FILL_RESPONSE_AFTER_RESET) - setResult(RESULT_OK, Intent().apply { - putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillResponse) - }) - finish() - } - } - } - - private fun showPackageInfo() { - runCatching { - with(binding) { - val packageInfo = - packageManager.getPackageInfo(appPackage, PackageManager.GET_META_DATA) - val installTime = DateUtils.getRelativeTimeSpanString(packageInfo.firstInstallTime) - warningAppInstallDate.text = - getString(R.string.oreo_autofill_warning_publisher_install_time, installTime) - val appInfo = - packageManager.getApplicationInfo(appPackage, PackageManager.GET_META_DATA) - warningAppName.text = "“${packageManager.getApplicationLabel(appInfo)}”" - - val currentHash = computeCertificatesHash(this@AutofillPublisherChangedActivity, appPackage) - warningAppAdvancedInfo.text = getString( - R.string.oreo_autofill_warning_publisher_advanced_info_template, - appPackage, - currentHash - ) - } - }.onFailure { e -> - e(e) { "Failed to retrieve package info for $appPackage" } - finish() - } - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt deleted file mode 100644 index bc012d3d..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/AutofillSaveActivity.kt +++ /dev/null @@ -1,149 +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.ui - -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.content.IntentSender -import android.os.Build -import android.os.Bundle -import android.view.autofill.AutofillManager -import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult -import androidx.annotation.RequiresApi -import androidx.appcompat.app.AppCompatActivity -import androidx.core.os.bundleOf -import com.github.ajalt.timberkt.e -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.AutofillResponseBuilder -import com.zeapo.pwdstore.crypto.PasswordCreationActivity -import com.zeapo.pwdstore.utils.PasswordRepository -import java.io.File - -@RequiresApi(Build.VERSION_CODES.O) -class AutofillSaveActivity : AppCompatActivity() { - - companion object { - - private const val EXTRA_FOLDER_NAME = - "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_FOLDER_NAME" - private const val EXTRA_PASSWORD = "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_PASSWORD" - private const val EXTRA_NAME = "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_NAME" - private const val EXTRA_SHOULD_MATCH_APP = - "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP" - private const val EXTRA_SHOULD_MATCH_WEB = - "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_SHOULD_MATCH_WEB" - private const val EXTRA_GENERATE_PASSWORD = - "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_GENERATE_PASSWORD" - - private var saveRequestCode = 1 - - fun makeSaveIntentSender( - context: Context, - credentials: Credentials?, - formOrigin: FormOrigin - ): IntentSender { - val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false) - // Prevent directory traversals - val sanitizedIdentifier = identifier.replace('\\', '_') - .replace('/', '_') - .trimStart('.') - .takeUnless { it.isBlank() } ?: formOrigin.identifier - val directoryStructure = AutofillPreferences.directoryStructure(context) - val folderName = directoryStructure.getSaveFolderName( - sanitizedIdentifier = sanitizedIdentifier, - username = credentials?.username - ) - val fileName = directoryStructure.getSaveFileName(username = credentials?.username, identifier = identifier) - val intent = Intent(context, AutofillSaveActivity::class.java).apply { - putExtras( - bundleOf( - EXTRA_FOLDER_NAME to folderName, - EXTRA_NAME to fileName, - EXTRA_PASSWORD to credentials?.password, - EXTRA_SHOULD_MATCH_APP to formOrigin.identifier.takeIf { formOrigin is FormOrigin.App }, - EXTRA_SHOULD_MATCH_WEB to formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web }, - EXTRA_GENERATE_PASSWORD to (credentials == null) - ) - ) - } - return PendingIntent.getActivity( - context, - saveRequestCode++, - intent, - PendingIntent.FLAG_CANCEL_CURRENT - ).intentSender - } - } - - private val formOrigin by lazy(LazyThreadSafetyMode.NONE) { - val shouldMatchApp: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_APP) - val shouldMatchWeb: String? = intent.getStringExtra(EXTRA_SHOULD_MATCH_WEB) - if (shouldMatchApp != null && shouldMatchWeb == null) { - FormOrigin.App(shouldMatchApp) - } else if (shouldMatchApp == null && shouldMatchWeb != null) { - FormOrigin.Web(shouldMatchWeb) - } else { - null - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - val repo = PasswordRepository.getRepositoryDirectory() - val saveIntent = Intent(this, PasswordCreationActivity::class.java).apply { - putExtras( - bundleOf( - "REPO_PATH" to repo.absolutePath, - "FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)!!).absolutePath, - PasswordCreationActivity.EXTRA_FILE_NAME to intent.getStringExtra(EXTRA_NAME), - PasswordCreationActivity.EXTRA_PASSWORD to intent.getStringExtra(EXTRA_PASSWORD), - PasswordCreationActivity.EXTRA_GENERATE_PASSWORD to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false) - ) - ) - } - registerForActivityResult(StartActivityForResult()) { result -> - val data = result.data - if (result.resultCode == RESULT_OK && data != null) { - val createdPath = data.getStringExtra("CREATED_FILE")!! - formOrigin?.let { - AutofillMatcher.addMatchFor(this, it, File(createdPath)) - } - val password = data.getStringExtra("PASSWORD") - val resultIntent = if (password != null) { - // Password was generated and should be filled into a form. - val username = data.getStringExtra("USERNAME") - val clientState = - intent?.getBundleExtra(AutofillManager.EXTRA_CLIENT_STATE) ?: run { - e { "AutofillDecryptActivity started without EXTRA_CLIENT_STATE" } - finish() - return@registerForActivityResult - } - val credentials = Credentials(username, password, null) - val fillInDataset = AutofillResponseBuilder.makeFillInDataset( - this, - credentials, - clientState, - AutofillAction.Generate - ) - Intent().apply { - putExtra(AutofillManager.EXTRA_AUTHENTICATION_RESULT, fillInDataset) - } - } else { - // Password was extracted from a form, there is nothing to fill. - Intent() - } - setResult(RESULT_OK, resultIntent) - } else { - setResult(RESULT_CANCELED) - } - finish() - }.launch(saveIntent) - } -} diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/PasswordViewHolder.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/PasswordViewHolder.kt deleted file mode 100644 index 162362f6..00000000 --- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/ui/PasswordViewHolder.kt +++ /dev/null @@ -1,16 +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.ui - -import android.view.View -import android.widget.TextView -import androidx.recyclerview.widget.RecyclerView -import com.zeapo.pwdstore.R - -class PasswordViewHolder(view: View) : RecyclerView.ViewHolder(view) { - - val title: TextView = itemView.findViewById(R.id.title) - val subtitle: TextView = itemView.findViewById(R.id.subtitle) -} |
