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:
Fabian Henneke 2020-03-25 18:13:04 +01:00 committed by GitHub
parent 973e023dda
commit fde16c60f4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 212 additions and 67 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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