Make preferred directory structure for Autofill configurable (#660)
Some users keep their password files in a directory structure such as: /example.org/john@doe.org.gpg while others prefer the style: /example.org/john@doe.org/password.gpg This commit adds a setting that allows to switch between the two. All Autofill operations, such as search, match, generate and save, respect this setting. Note: The first style seems to be the most widely used and is therefore kept as the default. The second style is mentioned on the official Pass website at: https://www.passwordstore.org/#organization
This commit is contained in:
parent
973e023dda
commit
fde16c60f4
11 changed files with 212 additions and 67 deletions
|
@ -62,8 +62,9 @@ class UserPreference : AppCompatActivity() {
|
||||||
private lateinit var prefsFragment: PrefsFragment
|
private lateinit var prefsFragment: PrefsFragment
|
||||||
|
|
||||||
class PrefsFragment : PreferenceFragmentCompat() {
|
class PrefsFragment : PreferenceFragmentCompat() {
|
||||||
private var autofillDependencies = listOf<Preference?>()
|
|
||||||
private var autoFillEnablePreference: SwitchPreferenceCompat? = null
|
private var autoFillEnablePreference: SwitchPreferenceCompat? = null
|
||||||
|
private lateinit var autofillDependencies: List<Preference>
|
||||||
|
private lateinit var oreoAutofillDependencies: List<Preference>
|
||||||
private lateinit var callingActivity: UserPreference
|
private lateinit var callingActivity: UserPreference
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
@ -96,16 +97,21 @@ class UserPreference : AppCompatActivity() {
|
||||||
|
|
||||||
// Autofill preferences
|
// Autofill preferences
|
||||||
autoFillEnablePreference = findPreference("autofill_enable")
|
autoFillEnablePreference = findPreference("autofill_enable")
|
||||||
val autoFillAppsPreference = findPreference<Preference>("autofill_apps")
|
val autoFillAppsPreference = findPreference<Preference>("autofill_apps")!!
|
||||||
val autoFillDefaultPreference = findPreference<CheckBoxPreference>("autofill_default")
|
val autoFillDefaultPreference = findPreference<CheckBoxPreference>("autofill_default")!!
|
||||||
val autoFillAlwaysShowDialogPreference = findPreference<CheckBoxPreference>("autofill_always")
|
val autoFillAlwaysShowDialogPreference =
|
||||||
val autoFillShowFullNamePreference = findPreference<CheckBoxPreference>("autofill_full_path")
|
findPreference<CheckBoxPreference>("autofill_always")!!
|
||||||
|
val autoFillShowFullNamePreference =
|
||||||
|
findPreference<CheckBoxPreference>("autofill_full_path")!!
|
||||||
autofillDependencies = listOf(
|
autofillDependencies = listOf(
|
||||||
autoFillAppsPreference,
|
autoFillAppsPreference,
|
||||||
autoFillDefaultPreference,
|
autoFillDefaultPreference,
|
||||||
autoFillAlwaysShowDialogPreference,
|
autoFillAlwaysShowDialogPreference,
|
||||||
autoFillShowFullNamePreference
|
autoFillShowFullNamePreference
|
||||||
)
|
)
|
||||||
|
val oreoAutofillDirectoryStructurePreference =
|
||||||
|
findPreference<ListPreference>("oreo_autofill_directory_structure")!!
|
||||||
|
oreoAutofillDependencies = listOf(oreoAutofillDirectoryStructurePreference)
|
||||||
|
|
||||||
// Misc preferences
|
// Misc preferences
|
||||||
val appVersionPreference = findPreference<Preference>("app_version")
|
val appVersionPreference = findPreference<Preference>("app_version")
|
||||||
|
@ -236,7 +242,7 @@ class UserPreference : AppCompatActivity() {
|
||||||
selectExternalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo
|
selectExternalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo
|
||||||
externalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo
|
externalGitRepositoryPreference?.onPreferenceChangeListener = resetRepo
|
||||||
|
|
||||||
autoFillAppsPreference?.onPreferenceClickListener = ClickListener {
|
autoFillAppsPreference.onPreferenceClickListener = ClickListener {
|
||||||
val intent = Intent(callingActivity, AutofillPreferenceActivity::class.java)
|
val intent = Intent(callingActivity, AutofillPreferenceActivity::class.java)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
true
|
true
|
||||||
|
@ -356,10 +362,14 @@ class UserPreference : AppCompatActivity() {
|
||||||
|
|
||||||
private fun updateAutofillSettings() {
|
private fun updateAutofillSettings() {
|
||||||
val isAccessibilityServiceEnabled = callingActivity.isAccessibilityServiceEnabled
|
val isAccessibilityServiceEnabled = callingActivity.isAccessibilityServiceEnabled
|
||||||
|
val isAutofillServiceEnabled = callingActivity.isAutofillServiceEnabled
|
||||||
autoFillEnablePreference?.isChecked =
|
autoFillEnablePreference?.isChecked =
|
||||||
isAccessibilityServiceEnabled || callingActivity.isAutofillServiceEnabled
|
isAccessibilityServiceEnabled || isAutofillServiceEnabled
|
||||||
autofillDependencies.forEach {
|
autofillDependencies.forEach {
|
||||||
it?.isVisible = isAccessibilityServiceEnabled
|
it.isVisible = isAccessibilityServiceEnabled
|
||||||
|
}
|
||||||
|
oreoAutofillDependencies.forEach {
|
||||||
|
it.isVisible = isAutofillServiceEnabled
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,16 +419,7 @@ class UserPreference : AppCompatActivity() {
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
}
|
}
|
||||||
setNegativeButton(R.string.dialog_cancel, null)
|
setNegativeButton(R.string.dialog_cancel, null)
|
||||||
setOnDismissListener {
|
setOnDismissListener { updateAutofillSettings() }
|
||||||
val isEnabled =
|
|
||||||
if (enableOreoAutofill && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
||||||
callingActivity.isAutofillServiceEnabled
|
|
||||||
} else {
|
|
||||||
callingActivity.isAccessibilityServiceEnabled
|
|
||||||
}
|
|
||||||
autoFillEnablePreference?.isChecked = isEnabled
|
|
||||||
autofillDependencies.forEach { it?.isVisible = isEnabled }
|
|
||||||
}
|
|
||||||
show()
|
show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,9 +73,14 @@ val AssistStructure.ViewNode.webOrigin: String?
|
||||||
|
|
||||||
data class Credentials(val username: String?, val password: String) {
|
data class Credentials(val username: String?, val password: String) {
|
||||||
companion object {
|
companion object {
|
||||||
fun fromStoreEntry(file: File, entry: PasswordEntry): Credentials {
|
fun fromStoreEntry(
|
||||||
return if (entry.hasUsername()) Credentials(entry.username, entry.password)
|
file: File,
|
||||||
else Credentials(file.nameWithoutExtension, entry.password)
|
entry: PasswordEntry,
|
||||||
|
directoryStructure: DirectoryStructure
|
||||||
|
): Credentials {
|
||||||
|
// Always give priority to a username stored in the encrypted extras
|
||||||
|
val username = entry.username ?: directoryStructure.getUsernameFor(file)
|
||||||
|
return Credentials(username, entry.password)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -95,7 +100,7 @@ private fun makeRemoteView(
|
||||||
|
|
||||||
fun makeFillMatchRemoteView(context: Context, file: File, formOrigin: FormOrigin): RemoteViews {
|
fun makeFillMatchRemoteView(context: Context, file: File, formOrigin: FormOrigin): RemoteViews {
|
||||||
val title = formOrigin.getPrettyIdentifier(context, untrusted = false)
|
val title = formOrigin.getPrettyIdentifier(context, untrusted = false)
|
||||||
val summary = file.nameWithoutExtension
|
val summary = AutofillPreferences.directoryStructure(context).getUsernameFor(file)
|
||||||
val iconRes = R.drawable.ic_person_black_24dp
|
val iconRes = R.drawable.ic_person_black_24dp
|
||||||
return makeRemoteView(context, title, summary, iconRes)
|
return makeRemoteView(context, title, summary, iconRes)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,59 @@
|
||||||
|
/*
|
||||||
|
* 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.os.Build
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import androidx.preference.PreferenceManager
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
private val Context.defaultSharedPreferences: SharedPreferences
|
||||||
|
get() = PreferenceManager.getDefaultSharedPreferences(this)
|
||||||
|
|
||||||
|
enum class DirectoryStructure(val value: String) {
|
||||||
|
FileBased("file"),
|
||||||
|
DirectoryBased("directory");
|
||||||
|
|
||||||
|
fun getUsernameFor(file: File) = when (this) {
|
||||||
|
FileBased -> file.nameWithoutExtension
|
||||||
|
DirectoryBased -> file.parentFile?.name ?: file.nameWithoutExtension
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getIdentifierFor(file: File) = when (this) {
|
||||||
|
FileBased -> file.parentFile?.name
|
||||||
|
DirectoryBased -> file.parentFile?.parentFile?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
fun getSaveFolderName(sanitizedIdentifier: String, username: String?) = when (this) {
|
||||||
|
FileBased -> sanitizedIdentifier
|
||||||
|
DirectoryBased -> Paths.get(sanitizedIdentifier, username ?: "username").toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getSaveFileName(username: String?) = when (this) {
|
||||||
|
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.defaultSharedPreferences.getString(DirectoryStructure.PREFERENCE, null)
|
||||||
|
return DirectoryStructure.fromValue(value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,9 @@ import com.github.ajalt.timberkt.d
|
||||||
import com.github.ajalt.timberkt.e
|
import com.github.ajalt.timberkt.e
|
||||||
import com.zeapo.pwdstore.PasswordEntry
|
import com.zeapo.pwdstore.PasswordEntry
|
||||||
import com.zeapo.pwdstore.autofill.oreo.AutofillAction
|
import com.zeapo.pwdstore.autofill.oreo.AutofillAction
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
|
||||||
import com.zeapo.pwdstore.autofill.oreo.Credentials
|
import com.zeapo.pwdstore.autofill.oreo.Credentials
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
|
||||||
import com.zeapo.pwdstore.autofill.oreo.FillableForm
|
import com.zeapo.pwdstore.autofill.oreo.FillableForm
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -76,6 +78,7 @@ class AutofillDecryptActivity : Activity(), CoroutineScope {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var continueAfterUserInteraction: Continuation<Intent>? = null
|
private var continueAfterUserInteraction: Continuation<Intent>? = null
|
||||||
|
private lateinit var directoryStructure: DirectoryStructure
|
||||||
|
|
||||||
override val coroutineContext
|
override val coroutineContext
|
||||||
get() = Dispatchers.IO + SupervisorJob()
|
get() = Dispatchers.IO + SupervisorJob()
|
||||||
|
@ -94,6 +97,7 @@ class AutofillDecryptActivity : Activity(), CoroutineScope {
|
||||||
}
|
}
|
||||||
val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!!
|
val isSearchAction = intent?.getBooleanExtra(EXTRA_SEARCH_ACTION, true)!!
|
||||||
val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match
|
val action = if (isSearchAction) AutofillAction.Search else AutofillAction.Match
|
||||||
|
directoryStructure = AutofillPreferences.directoryStructure(this)
|
||||||
d { action.toString() }
|
d { action.toString() }
|
||||||
launch {
|
launch {
|
||||||
val credentials = decryptUsernameAndPassword(File(filePath))
|
val credentials = decryptUsernameAndPassword(File(filePath))
|
||||||
|
@ -176,7 +180,7 @@ class AutofillDecryptActivity : Activity(), CoroutineScope {
|
||||||
val entry = withContext(Dispatchers.IO) {
|
val entry = withContext(Dispatchers.IO) {
|
||||||
PasswordEntry(decryptedOutput)
|
PasswordEntry(decryptedOutput)
|
||||||
}
|
}
|
||||||
Credentials.fromStoreEntry(file, entry)
|
Credentials.fromStoreEntry(file, entry, directoryStructure)
|
||||||
} catch (e: UnsupportedEncodingException) {
|
} catch (e: UnsupportedEncodingException) {
|
||||||
e(e) { "Failed to parse password entry" }
|
e(e) { "Failed to parse password entry" }
|
||||||
null
|
null
|
||||||
|
|
|
@ -23,10 +23,13 @@ import com.afollestad.recyclical.withItem
|
||||||
import com.github.ajalt.timberkt.e
|
import com.github.ajalt.timberkt.e
|
||||||
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.AutofillPreferences
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
|
||||||
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
|
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
|
||||||
import com.zeapo.pwdstore.utils.PasswordItem
|
import com.zeapo.pwdstore.utils.PasswordItem
|
||||||
import com.zeapo.pwdstore.utils.PasswordRepository
|
import com.zeapo.pwdstore.utils.PasswordRepository
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.nio.file.Paths
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlinx.android.synthetic.main.activity_oreo_autofill_filter.*
|
import kotlinx.android.synthetic.main.activity_oreo_autofill_filter.*
|
||||||
|
|
||||||
|
@ -69,6 +72,8 @@ class AutofillFilterView : AppCompatActivity() {
|
||||||
get() = PasswordRepository.PasswordSortOrder.getSortOrder(preferences)
|
get() = PasswordRepository.PasswordSortOrder.getSortOrder(preferences)
|
||||||
|
|
||||||
private lateinit var formOrigin: FormOrigin
|
private lateinit var formOrigin: FormOrigin
|
||||||
|
private lateinit var repositoryRoot: File
|
||||||
|
private lateinit var directoryStructure: DirectoryStructure
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
@ -98,6 +103,8 @@ class AutofillFilterView : AppCompatActivity() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
repositoryRoot = PasswordRepository.getRepositoryDirectory(this)
|
||||||
|
directoryStructure = AutofillPreferences.directoryStructure(this)
|
||||||
|
|
||||||
supportActionBar?.hide()
|
supportActionBar?.hide()
|
||||||
bindUI()
|
bindUI()
|
||||||
|
@ -110,9 +117,19 @@ class AutofillFilterView : AppCompatActivity() {
|
||||||
withDataSource(dataSource)
|
withDataSource(dataSource)
|
||||||
withItem<PasswordItem, PasswordViewHolder>(R.layout.oreo_autofill_filter_row) {
|
withItem<PasswordItem, PasswordViewHolder>(R.layout.oreo_autofill_filter_row) {
|
||||||
onBind(::PasswordViewHolder) { _, item ->
|
onBind(::PasswordViewHolder) { _, item ->
|
||||||
title.text = item.fullPathToParent
|
when (directoryStructure) {
|
||||||
// drop the .gpg extension
|
DirectoryStructure.FileBased -> {
|
||||||
subtitle.text = item.name.dropLast(4)
|
title.text = item.file.relativeTo(item.rootDir).parent
|
||||||
|
subtitle.text = item.file.nameWithoutExtension
|
||||||
|
}
|
||||||
|
DirectoryStructure.DirectoryBased -> {
|
||||||
|
title.text =
|
||||||
|
item.file.relativeTo(item.rootDir).parentFile?.parent ?: "/INVALID"
|
||||||
|
subtitle.text =
|
||||||
|
Paths.get(item.file.parentFile.name, item.file.nameWithoutExtension)
|
||||||
|
.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
onClick { decryptAndFill(item) }
|
onClick { decryptAndFill(item) }
|
||||||
}
|
}
|
||||||
|
@ -156,37 +173,37 @@ class AutofillFilterView : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun File.matches(filter: String, strict: Boolean): Boolean {
|
||||||
|
return if (strict) {
|
||||||
|
val toMatch = directoryStructure.getIdentifierFor(this) ?: return false
|
||||||
|
// In strict mode, we match
|
||||||
|
// * the search term exactly,
|
||||||
|
// * subdomains of the search term,
|
||||||
|
// * or the search term plus an arbitrary protocol.
|
||||||
|
toMatch == filter || toMatch.endsWith(".$filter") || toMatch.endsWith("://$filter")
|
||||||
|
} else {
|
||||||
|
val toMatch =
|
||||||
|
"${relativeTo(repositoryRoot).path}/$nameWithoutExtension".toLowerCase(Locale.getDefault())
|
||||||
|
toMatch.contains(filter.toLowerCase(Locale.getDefault()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun recursiveFilter(filter: String, dir: File? = null, strict: Boolean = true) {
|
private fun recursiveFilter(filter: String, dir: File? = null, strict: Boolean = true) {
|
||||||
val root = PasswordRepository.getRepositoryDirectory(this)
|
|
||||||
// on the root the pathStack is empty
|
// on the root the pathStack is empty
|
||||||
val passwordItems = if (dir == null) {
|
val passwordItems = if (dir == null) {
|
||||||
PasswordRepository.getPasswords(
|
PasswordRepository.getPasswords(repositoryRoot, sortOrder)
|
||||||
PasswordRepository.getRepositoryDirectory(this),
|
|
||||||
sortOrder
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
PasswordRepository.getPasswords(
|
PasswordRepository.getPasswords(dir, repositoryRoot, sortOrder)
|
||||||
dir,
|
|
||||||
PasswordRepository.getRepositoryDirectory(this),
|
|
||||||
sortOrder
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (item in passwordItems) {
|
for (item in passwordItems) {
|
||||||
if (item.type == PasswordItem.TYPE_CATEGORY) {
|
if (item.type == PasswordItem.TYPE_CATEGORY) {
|
||||||
recursiveFilter(filter, item.file, strict = strict)
|
recursiveFilter(filter, item.file, strict = strict)
|
||||||
}
|
} else {
|
||||||
|
|
||||||
// TODO: Implement fuzzy search if strict == false?
|
// TODO: Implement fuzzy search if strict == false?
|
||||||
val matches = if (strict) item.file.parentFile.name.let {
|
val matches = item.file.matches(filter, strict = strict)
|
||||||
it == filter || it.endsWith(".$filter") || it.endsWith("://$filter")
|
|
||||||
}
|
|
||||||
else "${item.file.relativeTo(root).path}/${item.file.nameWithoutExtension}".toLowerCase(
|
|
||||||
Locale.getDefault()
|
|
||||||
).contains(filter.toLowerCase(Locale.getDefault()))
|
|
||||||
|
|
||||||
val inAdapter = dataSource.contains(item)
|
val inAdapter = dataSource.contains(item)
|
||||||
if (item.type == PasswordItem.TYPE_PASSWORD && matches && !inAdapter) {
|
if (matches && !inAdapter) {
|
||||||
dataSource.add(item)
|
dataSource.add(item)
|
||||||
} else if (!matches && inAdapter) {
|
} else if (!matches && inAdapter) {
|
||||||
dataSource.remove(item)
|
dataSource.remove(item)
|
||||||
|
@ -194,3 +211,4 @@ class AutofillFilterView : AppCompatActivity() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@ import com.github.ajalt.timberkt.e
|
||||||
import com.zeapo.pwdstore.PasswordStore
|
import com.zeapo.pwdstore.PasswordStore
|
||||||
import com.zeapo.pwdstore.autofill.oreo.AutofillAction
|
import com.zeapo.pwdstore.autofill.oreo.AutofillAction
|
||||||
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.Credentials
|
import com.zeapo.pwdstore.autofill.oreo.Credentials
|
||||||
import com.zeapo.pwdstore.autofill.oreo.FillableForm
|
import com.zeapo.pwdstore.autofill.oreo.FillableForm
|
||||||
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
|
import com.zeapo.pwdstore.autofill.oreo.FormOrigin
|
||||||
|
@ -32,7 +33,7 @@ class AutofillSaveActivity : Activity() {
|
||||||
private const val EXTRA_FOLDER_NAME =
|
private const val EXTRA_FOLDER_NAME =
|
||||||
"com.zeapo.pwdstore.autofill.oreo.ui.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_PASSWORD = "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_PASSWORD"
|
||||||
private const val EXTRA_USERNAME = "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_USERNAME"
|
private const val EXTRA_NAME = "com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_NAME"
|
||||||
private const val EXTRA_SHOULD_MATCH_APP =
|
private const val EXTRA_SHOULD_MATCH_APP =
|
||||||
"com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP"
|
"com.zeapo.pwdstore.autofill.oreo.ui.EXTRA_SHOULD_MATCH_APP"
|
||||||
private const val EXTRA_SHOULD_MATCH_WEB =
|
private const val EXTRA_SHOULD_MATCH_WEB =
|
||||||
|
@ -48,15 +49,23 @@ class AutofillSaveActivity : Activity() {
|
||||||
formOrigin: FormOrigin
|
formOrigin: FormOrigin
|
||||||
): IntentSender {
|
): IntentSender {
|
||||||
val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false)
|
val identifier = formOrigin.getPrettyIdentifier(context, untrusted = false)
|
||||||
val sanitizedIdentifier = identifier.replace("""[\\\/]""", "")
|
// Prevent directory traversals
|
||||||
val folderName =
|
val sanitizedIdentifier = identifier.replace('\\', '_')
|
||||||
sanitizedIdentifier.takeUnless { it.isBlank() } ?: formOrigin.identifier
|
.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)
|
||||||
val intent = Intent(context, AutofillSaveActivity::class.java).apply {
|
val intent = Intent(context, AutofillSaveActivity::class.java).apply {
|
||||||
putExtras(
|
putExtras(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
EXTRA_FOLDER_NAME to folderName,
|
EXTRA_FOLDER_NAME to folderName,
|
||||||
|
EXTRA_NAME to fileName,
|
||||||
EXTRA_PASSWORD to credentials?.password,
|
EXTRA_PASSWORD to credentials?.password,
|
||||||
EXTRA_USERNAME to credentials?.username,
|
|
||||||
EXTRA_SHOULD_MATCH_APP to formOrigin.identifier.takeIf { formOrigin is FormOrigin.App },
|
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_SHOULD_MATCH_WEB to formOrigin.identifier.takeIf { formOrigin is FormOrigin.Web },
|
||||||
EXTRA_GENERATE_PASSWORD to (credentials == null)
|
EXTRA_GENERATE_PASSWORD to (credentials == null)
|
||||||
|
@ -87,15 +96,13 @@ class AutofillSaveActivity : Activity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val repo = PasswordRepository.getRepositoryDirectory(applicationContext)
|
val repo = PasswordRepository.getRepositoryDirectory(applicationContext)
|
||||||
val username = intent.getStringExtra(EXTRA_USERNAME)
|
|
||||||
|
|
||||||
val saveIntent = Intent(this, PgpActivity::class.java).apply {
|
val saveIntent = Intent(this, PgpActivity::class.java).apply {
|
||||||
putExtras(
|
putExtras(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
"REPO_PATH" to repo.absolutePath,
|
"REPO_PATH" to repo.absolutePath,
|
||||||
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)).absolutePath,
|
"FILE_PATH" to repo.resolve(intent.getStringExtra(EXTRA_FOLDER_NAME)).absolutePath,
|
||||||
"OPERATION" to "ENCRYPT",
|
"OPERATION" to "ENCRYPT",
|
||||||
"SUGGESTED_NAME" to username,
|
"SUGGESTED_NAME" to intent.getStringExtra(EXTRA_NAME),
|
||||||
"SUGGESTED_PASS" to intent.getStringExtra(EXTRA_PASSWORD),
|
"SUGGESTED_PASS" to intent.getStringExtra(EXTRA_PASSWORD),
|
||||||
"GENERATE_PASSWORD" to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
|
"GENERATE_PASSWORD" to intent.getBooleanExtra(EXTRA_GENERATE_PASSWORD, false)
|
||||||
)
|
)
|
||||||
|
|
|
@ -41,6 +41,8 @@ import com.zeapo.pwdstore.ClipboardService
|
||||||
import com.zeapo.pwdstore.PasswordEntry
|
import com.zeapo.pwdstore.PasswordEntry
|
||||||
import com.zeapo.pwdstore.R
|
import com.zeapo.pwdstore.R
|
||||||
import com.zeapo.pwdstore.UserPreference
|
import com.zeapo.pwdstore.UserPreference
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.AutofillPreferences
|
||||||
|
import com.zeapo.pwdstore.autofill.oreo.DirectoryStructure
|
||||||
import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment
|
import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment
|
||||||
import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
|
import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
|
||||||
import com.zeapo.pwdstore.utils.Otp
|
import com.zeapo.pwdstore.utils.Otp
|
||||||
|
@ -157,9 +159,23 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
||||||
}
|
}
|
||||||
|
|
||||||
title = getString(R.string.new_password_title)
|
title = getString(R.string.new_password_title)
|
||||||
crypto_password_category.text = getRelativePath(fullPath, repoPath)
|
crypto_password_category.apply {
|
||||||
suggestedName?.let {
|
setText(getRelativePath(fullPath, repoPath))
|
||||||
crypto_password_file_edit.setText(it)
|
// If the activity has been provided with suggested info, we allow the user to
|
||||||
|
// edit the path, otherwise we style the EditText like a TextView.
|
||||||
|
if (suggestedName != null) {
|
||||||
|
isEnabled = true
|
||||||
|
} else {
|
||||||
|
setBackgroundColor(getColor(android.R.color.transparent))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suggestedName?.let { crypto_password_file_edit.setText(it) }
|
||||||
|
// Allow the user to quickly switch between storing the username as the filename or
|
||||||
|
// in the encrypted extras. This only makes sense if the directory structure is
|
||||||
|
// FileBased.
|
||||||
|
if (suggestedName != null &&
|
||||||
|
AutofillPreferences.directoryStructure(this) == DirectoryStructure.FileBased
|
||||||
|
) {
|
||||||
encrypt_username.apply {
|
encrypt_username.apply {
|
||||||
visibility = View.VISIBLE
|
visibility = View.VISIBLE
|
||||||
setOnClickListener {
|
setOnClickListener {
|
||||||
|
@ -549,7 +565,20 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
||||||
val iStream = ByteArrayInputStream(content.toByteArray(Charset.forName("UTF-8")))
|
val iStream = ByteArrayInputStream(content.toByteArray(Charset.forName("UTF-8")))
|
||||||
val oStream = ByteArrayOutputStream()
|
val oStream = ByteArrayOutputStream()
|
||||||
|
|
||||||
val path = if (intent.getBooleanExtra("fromDecrypt", false)) fullPath else "$fullPath/$editName.gpg"
|
val path = when {
|
||||||
|
intent.getBooleanExtra("fromDecrypt", false) -> fullPath
|
||||||
|
// If we allowed the user to edit the relative path, we have to consider it here instead
|
||||||
|
// of fullPath.
|
||||||
|
crypto_password_category.isEnabled -> {
|
||||||
|
val editRelativePath = crypto_password_category.text!!.toString().trim()
|
||||||
|
if (editRelativePath.isEmpty()) {
|
||||||
|
showSnackbar(resources.getString(R.string.path_toast_text))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
"$repoPath/${editRelativePath.trim('/')}/$editName.gpg"
|
||||||
|
}
|
||||||
|
else -> "$fullPath/$editName.gpg"
|
||||||
|
}
|
||||||
|
|
||||||
lifecycleScope.launch(IO) {
|
lifecycleScope.launch(IO) {
|
||||||
api?.executeApiAsync(data, iStream, oStream, object : OpenPgpApi.IOpenPgpCallback {
|
api?.executeApiAsync(data, iStream, oStream, object : OpenPgpApi.IOpenPgpCallback {
|
||||||
|
@ -575,9 +604,14 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (shouldGeneratePassword) {
|
if (shouldGeneratePassword) {
|
||||||
|
val directoryStructure =
|
||||||
|
AutofillPreferences.directoryStructure(applicationContext)
|
||||||
val entry = PasswordEntry(content)
|
val entry = PasswordEntry(content)
|
||||||
returnIntent.putExtra("PASSWORD", entry.password)
|
returnIntent.putExtra("PASSWORD", entry.password)
|
||||||
returnIntent.putExtra("USERNAME", entry.username ?: file.nameWithoutExtension)
|
returnIntent.putExtra(
|
||||||
|
"USERNAME",
|
||||||
|
directoryStructure.getUsernameFor(file)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setResult(RESULT_OK, returnIntent)
|
setResult(RESULT_OK, returnIntent)
|
||||||
|
@ -615,7 +649,7 @@ class PgpActivity : AppCompatActivity(), OpenPgpServiceConnection.OnBound {
|
||||||
crypto_extra_edit.setText(passwordEntry?.extraContent)
|
crypto_extra_edit.setText(passwordEntry?.extraContent)
|
||||||
crypto_extra_edit.typeface = monoTypeface
|
crypto_extra_edit.typeface = monoTypeface
|
||||||
|
|
||||||
crypto_password_category.text = relativeParentPath
|
crypto_password_category.setText(relativeParentPath)
|
||||||
crypto_password_file_edit.setText(name)
|
crypto_password_file_edit.setText(name)
|
||||||
crypto_password_file_edit.isEnabled = false
|
crypto_password_file_edit.isEnabled = false
|
||||||
|
|
||||||
|
|
|
@ -9,13 +9,13 @@
|
||||||
android:padding="@dimen/activity_horizontal_margin"
|
android:padding="@dimen/activity_horizontal_margin"
|
||||||
tools:context="com.zeapo.pwdstore.crypto.PgpActivity">
|
tools:context="com.zeapo.pwdstore.crypto.PgpActivity">
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatTextView
|
<androidx.appcompat.widget.AppCompatEditText
|
||||||
android:id="@+id/crypto_password_category"
|
android:id="@+id/crypto_password_category"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginStart="@dimen/activity_horizontal_margin"
|
android:layout_marginStart="@dimen/activity_horizontal_margin"
|
||||||
android:textColor="?android:attr/textColor"
|
android:textColor="?android:attr/textColor"
|
||||||
android:textIsSelectable="false"
|
android:enabled="false"
|
||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
|
|
@ -51,5 +51,13 @@
|
||||||
<item>classic</item>
|
<item>classic</item>
|
||||||
<item>xkpasswd</item>
|
<item>xkpasswd</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
<string-array name="oreo_autofill_directory_structure_entries">
|
||||||
|
<item>/example.org/john@doe.org(.gpg)</item>
|
||||||
|
<item>/example.org/john@doe.org/password(.gpg)</item>
|
||||||
|
</string-array>
|
||||||
|
<string-array name="oreo_autofill_directory_structure_values">
|
||||||
|
<item>file</item>
|
||||||
|
<item>directory</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -42,6 +42,7 @@
|
||||||
<string name="clipboard_username_toast_text">Username copied to clipboard</string>
|
<string name="clipboard_username_toast_text">Username copied to clipboard</string>
|
||||||
<string name="clipboard_otp_toast_text">OTP code copied to clipboard</string>
|
<string name="clipboard_otp_toast_text">OTP code copied to clipboard</string>
|
||||||
<string name="file_toast_text">Please provide a file name</string>
|
<string name="file_toast_text">Please provide a file name</string>
|
||||||
|
<string name="path_toast_text">Please provide a file path</string>
|
||||||
<string name="empty_toast_text">You cannot use an empty password or empty extra content</string>
|
<string name="empty_toast_text">You cannot use an empty password or empty extra content</string>
|
||||||
|
|
||||||
<!-- Git Async Task -->
|
<!-- Git Async Task -->
|
||||||
|
@ -268,6 +269,7 @@
|
||||||
<string name="oreo_autofill_fill_support">Fill credentials</string>
|
<string name="oreo_autofill_fill_support">Fill credentials</string>
|
||||||
<string name="oreo_autofill_flaky_fill_support">Fill credentials (may require restarting the browser from time to time)</string>
|
<string name="oreo_autofill_flaky_fill_support">Fill credentials (may require restarting the browser from time to time)</string>
|
||||||
<string name="oreo_autofill_no_support">No support</string>
|
<string name="oreo_autofill_no_support">No support</string>
|
||||||
|
<string name="oreo_autofill_preference_directory_structure">Password file organization</string>
|
||||||
|
|
||||||
<!-- Autofill -->
|
<!-- Autofill -->
|
||||||
<string name="autofill_description">Autofills password fields in apps. Only works for Android versions 4.3 and up. Does not rely on the clipboard for Android versions 5.0 and up.</string>
|
<string name="autofill_description">Autofills password fields in apps. Only works for Android versions 4.3 and up. Does not rely on the clipboard for Android versions 5.0 and up.</string>
|
||||||
|
|
|
@ -6,6 +6,13 @@
|
||||||
app:defaultValue="true"
|
app:defaultValue="true"
|
||||||
app:key="autofill_enable"
|
app:key="autofill_enable"
|
||||||
app:title="@string/pref_autofill_enable_title"/>
|
app:title="@string/pref_autofill_enable_title"/>
|
||||||
|
<androidx.preference.ListPreference
|
||||||
|
app:defaultValue="file"
|
||||||
|
app:entries="@array/oreo_autofill_directory_structure_entries"
|
||||||
|
app:entryValues="@array/oreo_autofill_directory_structure_values"
|
||||||
|
app:key="oreo_autofill_directory_structure"
|
||||||
|
app:title="@string/oreo_autofill_preference_directory_structure"
|
||||||
|
app:useSimpleSummaryProvider="true" />
|
||||||
<androidx.preference.Preference
|
<androidx.preference.Preference
|
||||||
app:key="autofill_apps"
|
app:key="autofill_apps"
|
||||||
app:title="@string/pref_autofill_apps_title"/>
|
app:title="@string/pref_autofill_apps_title"/>
|
||||||
|
|
Loading…
Reference in a new issue