aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/dev/msfjarvis/aps/util/settings
diff options
context:
space:
mode:
authorHarsh Shandilya <me@msfjarvis.dev>2020-12-05 06:07:18 +0530
committerGitHub <noreply@github.com>2020-12-05 06:07:18 +0530
commit5e66d99c852ea67a88b650c03b0e8d55e83eccde (patch)
treeaee323a08253fe7b863976eff92a1bd412bd1430 /app/src/main/java/dev/msfjarvis/aps/util/settings
parent8eb55f18a11d6b2f155c7c9d48ca833313ee13db (diff)
downloadHardenedPass-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/dev/msfjarvis/aps/util/settings')
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/settings/GitSettings.kt181
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/settings/Migrations.kt114
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/settings/PasswordSortOrder.kt49
-rw-r--r--app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt85
4 files changed, 429 insertions, 0 deletions
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/settings/GitSettings.kt b/app/src/main/java/dev/msfjarvis/aps/util/settings/GitSettings.kt
new file mode 100644
index 00000000..864cbf81
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/util/settings/GitSettings.kt
@@ -0,0 +1,181 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package dev.msfjarvis.aps.util.settings
+
+import androidx.core.content.edit
+import com.github.michaelbull.result.getOrElse
+import com.github.michaelbull.result.runCatching
+import dev.msfjarvis.aps.Application
+import dev.msfjarvis.aps.data.repo.PasswordRepository
+import dev.msfjarvis.aps.util.extensions.getEncryptedGitPrefs
+import dev.msfjarvis.aps.util.extensions.getEncryptedProxyPrefs
+import dev.msfjarvis.aps.util.extensions.getString
+import dev.msfjarvis.aps.util.extensions.sharedPrefs
+import java.io.File
+import org.eclipse.jgit.transport.URIish
+
+enum class Protocol(val pref: String) {
+ Ssh("ssh://"),
+ Https("https://"),
+ ;
+
+ companion object {
+
+ private val map = values().associateBy(Protocol::pref)
+ fun fromString(type: String?): Protocol {
+ return map[type ?: return Ssh]
+ ?: throw IllegalArgumentException("$type is not a valid Protocol")
+ }
+ }
+}
+
+enum class AuthMode(val pref: String) {
+ SshKey("ssh-key"),
+ Password("username/password"),
+ OpenKeychain("OpenKeychain"),
+ None("None"),
+ ;
+
+ companion object {
+
+ private val map = values().associateBy(AuthMode::pref)
+ fun fromString(type: String?): AuthMode {
+ return map[type ?: return SshKey]
+ ?: throw IllegalArgumentException("$type is not a valid AuthMode")
+ }
+ }
+}
+
+object GitSettings {
+
+ private const val DEFAULT_BRANCH = "master"
+
+ private val settings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.sharedPrefs }
+ private val encryptedSettings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.getEncryptedGitPrefs() }
+ private val proxySettings by lazy(LazyThreadSafetyMode.PUBLICATION) { Application.instance.getEncryptedProxyPrefs() }
+
+ var authMode
+ get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH))
+ private set(value) {
+ settings.edit {
+ putString(PreferenceKeys.GIT_REMOTE_AUTH, value.pref)
+ }
+ }
+ var url
+ get() = settings.getString(PreferenceKeys.GIT_REMOTE_URL)
+ private set(value) {
+ require(value != null)
+ if (value == url)
+ return
+ settings.edit {
+ putString(PreferenceKeys.GIT_REMOTE_URL, value)
+ }
+ if (PasswordRepository.isInitialized)
+ PasswordRepository.addRemote("origin", value, true)
+ // When the server changes, remote password, multiplexing support and host key file
+ // should be deleted/reset.
+ useMultiplexing = true
+ encryptedSettings.edit { remove(PreferenceKeys.HTTPS_PASSWORD) }
+ File("${Application.instance.filesDir}/.host_key").delete()
+ }
+ var authorName
+ get() = settings.getString(PreferenceKeys.GIT_CONFIG_AUTHOR_NAME) ?: ""
+ set(value) {
+ settings.edit {
+ putString(PreferenceKeys.GIT_CONFIG_AUTHOR_NAME, value)
+ }
+ }
+ var authorEmail
+ get() = settings.getString(PreferenceKeys.GIT_CONFIG_AUTHOR_EMAIL) ?: ""
+ set(value) {
+ settings.edit {
+ putString(PreferenceKeys.GIT_CONFIG_AUTHOR_EMAIL, value)
+ }
+ }
+ var branch
+ get() = settings.getString(PreferenceKeys.GIT_BRANCH_NAME) ?: DEFAULT_BRANCH
+ private set(value) {
+ settings.edit {
+ putString(PreferenceKeys.GIT_BRANCH_NAME, value)
+ }
+ }
+ var useMultiplexing
+ get() = settings.getBoolean(PreferenceKeys.GIT_REMOTE_USE_MULTIPLEXING, true)
+ set(value) {
+ settings.edit {
+ putBoolean(PreferenceKeys.GIT_REMOTE_USE_MULTIPLEXING, value)
+ }
+ }
+
+ var proxyHost
+ get() = proxySettings.getString(PreferenceKeys.PROXY_HOST)
+ set(value) {
+ proxySettings.edit {
+ putString(PreferenceKeys.PROXY_HOST, value)
+ }
+ }
+
+ var proxyPort
+ get() = proxySettings.getInt(PreferenceKeys.PROXY_PORT, -1)
+ set(value) {
+ proxySettings.edit {
+ putInt(PreferenceKeys.PROXY_PORT, value)
+ }
+ }
+
+ var proxyUsername
+ get() = settings.getString(PreferenceKeys.PROXY_USERNAME)
+ set(value) {
+ proxySettings.edit {
+ putString(PreferenceKeys.PROXY_USERNAME, value)
+ }
+ }
+
+ var proxyPassword
+ get() = proxySettings.getString(PreferenceKeys.PROXY_PASSWORD)
+ set(value) {
+ proxySettings.edit {
+ putString(PreferenceKeys.PROXY_PASSWORD, value)
+ }
+ }
+
+ sealed class UpdateConnectionSettingsResult {
+ class MissingUsername(val newProtocol: Protocol) : UpdateConnectionSettingsResult()
+ class AuthModeMismatch(val newProtocol: Protocol, val validModes: List<AuthMode>) : UpdateConnectionSettingsResult()
+ object Valid : UpdateConnectionSettingsResult()
+ object FailedToParseUrl : UpdateConnectionSettingsResult()
+ }
+
+ fun updateConnectionSettingsIfValid(newAuthMode: AuthMode, newUrl: String, newBranch: String): UpdateConnectionSettingsResult {
+ val parsedUrl = runCatching {
+ URIish(newUrl)
+ }.getOrElse {
+ return UpdateConnectionSettingsResult.FailedToParseUrl
+ }
+ val newProtocol = when (parsedUrl.scheme) {
+ in listOf("http", "https") -> Protocol.Https
+ in listOf("ssh", null) -> Protocol.Ssh
+ else -> return UpdateConnectionSettingsResult.FailedToParseUrl
+ }
+ if (newAuthMode != AuthMode.None && parsedUrl.user.isNullOrBlank())
+ return UpdateConnectionSettingsResult.MissingUsername(newProtocol)
+
+ val validHttpsAuth = listOf(AuthMode.None, AuthMode.Password)
+ val validSshAuth = listOf(AuthMode.OpenKeychain, AuthMode.Password, AuthMode.SshKey)
+ when {
+ newProtocol == Protocol.Https && newAuthMode !in validHttpsAuth -> {
+ return UpdateConnectionSettingsResult.AuthModeMismatch(newProtocol, validHttpsAuth)
+ }
+ newProtocol == Protocol.Ssh && newAuthMode !in validSshAuth -> {
+ return UpdateConnectionSettingsResult.AuthModeMismatch(newProtocol, validSshAuth)
+ }
+ }
+
+ url = newUrl
+ authMode = newAuthMode
+ branch = newBranch
+ return UpdateConnectionSettingsResult.Valid
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/settings/Migrations.kt b/app/src/main/java/dev/msfjarvis/aps/util/settings/Migrations.kt
new file mode 100644
index 00000000..fc506995
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/util/settings/Migrations.kt
@@ -0,0 +1,114 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+@file:Suppress("DEPRECATION")
+
+package dev.msfjarvis.aps.util.settings
+
+import android.content.Context
+import android.content.SharedPreferences
+import androidx.core.content.edit
+import com.github.ajalt.timberkt.e
+import com.github.ajalt.timberkt.i
+import com.github.michaelbull.result.get
+import com.github.michaelbull.result.runCatching
+import dev.msfjarvis.aps.util.git.sshj.SshKey
+import dev.msfjarvis.aps.util.extensions.getString
+import dev.msfjarvis.aps.util.extensions.sharedPrefs
+import java.io.File
+import java.net.URI
+
+fun runMigrations(context: Context) {
+ val sharedPrefs = context.sharedPrefs
+ migrateToGitUrlBasedConfig(sharedPrefs)
+ migrateToHideAll(sharedPrefs)
+ migrateToSshKey(context, sharedPrefs)
+}
+
+private fun migrateToGitUrlBasedConfig(sharedPrefs: SharedPreferences) {
+ val serverHostname = sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_SERVER)
+ ?: return
+ i { "Migrating to URL-based Git config" }
+ val serverPort = sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_PORT) ?: ""
+ val serverUser = sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_USERNAME) ?: ""
+ val serverPath = sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_LOCATION) ?: ""
+ val protocol = Protocol.fromString(sharedPrefs.getString(PreferenceKeys.GIT_REMOTE_PROTOCOL))
+
+ // Whether we need the leading ssh:// depends on the use of a custom port.
+ val hostnamePart = serverHostname.removePrefix("ssh://")
+ val url = when (protocol) {
+ Protocol.Ssh -> {
+ val userPart = if (serverUser.isEmpty()) "" else "${serverUser.trimEnd('@')}@"
+ val portPart =
+ if (serverPort == "22" || serverPort.isEmpty()) "" else ":$serverPort"
+ if (portPart.isEmpty()) {
+ "$userPart$hostnamePart:$serverPath"
+ } else {
+ // Only absolute paths are supported with custom ports.
+ if (!serverPath.startsWith('/'))
+ null
+ else
+ // We have to specify the ssh scheme as this is the only way to pass a custom
+ // port.
+ "ssh://$userPart$hostnamePart$portPart$serverPath"
+ }
+ }
+ Protocol.Https -> {
+ val portPart =
+ if (serverPort == "443" || serverPort.isEmpty()) "" else ":$serverPort"
+ val pathPart = serverPath.trimStart('/', ':')
+ val urlWithFreeEntryScheme = "$hostnamePart$portPart/$pathPart"
+ val url = when {
+ urlWithFreeEntryScheme.startsWith("https://") -> urlWithFreeEntryScheme
+ urlWithFreeEntryScheme.startsWith("http://") -> urlWithFreeEntryScheme.replaceFirst("http", "https")
+ else -> "https://$urlWithFreeEntryScheme"
+ }
+ runCatching {
+ if (URI(url).rawAuthority != null)
+ url
+ else
+ null
+ }.get()
+ }
+ }
+
+ sharedPrefs.edit {
+ remove(PreferenceKeys.GIT_REMOTE_LOCATION)
+ remove(PreferenceKeys.GIT_REMOTE_PORT)
+ remove(PreferenceKeys.GIT_REMOTE_SERVER)
+ remove(PreferenceKeys.GIT_REMOTE_USERNAME)
+ remove(PreferenceKeys.GIT_REMOTE_PROTOCOL)
+ }
+ if (url == null || GitSettings.updateConnectionSettingsIfValid(
+ newAuthMode = GitSettings.authMode,
+ newUrl = url,
+ newBranch = GitSettings.branch) != GitSettings.UpdateConnectionSettingsResult.Valid) {
+ e { "Failed to migrate to URL-based Git config, generated URL is invalid" }
+ }
+}
+
+private fun migrateToHideAll(sharedPrefs: SharedPreferences) {
+ sharedPrefs.all[PreferenceKeys.SHOW_HIDDEN_FOLDERS] ?: return
+ val isHidden = sharedPrefs.getBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, false)
+ sharedPrefs.edit {
+ remove(PreferenceKeys.SHOW_HIDDEN_FOLDERS)
+ putBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, isHidden)
+ }
+}
+
+private fun migrateToSshKey(context: Context, sharedPrefs: SharedPreferences) {
+ val privateKeyFile = File(context.filesDir, ".ssh_key")
+ if (sharedPrefs.contains(PreferenceKeys.USE_GENERATED_KEY) &&
+ !SshKey.exists &&
+ privateKeyFile.exists()) {
+ // Currently uses a private key imported or generated with an old version of Password Store.
+ // Generated keys come with a public key which the user should still be able to view after
+ // the migration (not possible for regular imported keys), hence the special case.
+ val isGeneratedKey = sharedPrefs.getBoolean(PreferenceKeys.USE_GENERATED_KEY, false)
+ SshKey.useLegacyKey(isGeneratedKey)
+ sharedPrefs.edit {
+ remove(PreferenceKeys.USE_GENERATED_KEY)
+ }
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/settings/PasswordSortOrder.kt b/app/src/main/java/dev/msfjarvis/aps/util/settings/PasswordSortOrder.kt
new file mode 100644
index 00000000..f5a639f0
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/util/settings/PasswordSortOrder.kt
@@ -0,0 +1,49 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.util.settings
+
+import android.content.Context
+import android.content.SharedPreferences
+import dev.msfjarvis.aps.Application
+import dev.msfjarvis.aps.data.password.PasswordItem
+import dev.msfjarvis.aps.util.extensions.base64
+import dev.msfjarvis.aps.util.extensions.getString
+
+enum class PasswordSortOrder(val comparator: java.util.Comparator<PasswordItem>) {
+
+ FOLDER_FIRST(Comparator { p1: PasswordItem, p2: PasswordItem ->
+ (p1.type + p1.name)
+ .compareTo(p2.type + p2.name, ignoreCase = true)
+ }),
+
+ INDEPENDENT(Comparator { p1: PasswordItem, p2: PasswordItem ->
+ p1.name.compareTo(p2.name, ignoreCase = true)
+ }),
+
+ RECENTLY_USED(Comparator { p1: PasswordItem, p2: PasswordItem ->
+ val recentHistory = Application.instance.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
+ val timeP1 = recentHistory.getString(p1.file.absolutePath.base64())
+ val timeP2 = recentHistory.getString(p2.file.absolutePath.base64())
+ when {
+ timeP1 != null && timeP2 != null -> timeP2.compareTo(timeP1)
+ timeP1 != null && timeP2 == null -> return@Comparator -1
+ timeP1 == null && timeP2 != null -> return@Comparator 1
+ else -> p1.name.compareTo(p2.name, ignoreCase = true)
+ }
+ }),
+
+ FILE_FIRST(Comparator { p1: PasswordItem, p2: PasswordItem ->
+ (p2.type + p1.name).compareTo(p1.type + p2.name, ignoreCase = true)
+ });
+
+ companion object {
+
+ @JvmStatic
+ fun getSortOrder(settings: SharedPreferences): PasswordSortOrder {
+ return valueOf(settings.getString(PreferenceKeys.SORT_ORDER) ?: FOLDER_FIRST.name)
+ }
+ }
+}
diff --git a/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt b/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt
new file mode 100644
index 00000000..198be889
--- /dev/null
+++ b/app/src/main/java/dev/msfjarvis/aps/util/settings/PreferenceKeys.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+
+package dev.msfjarvis.aps.util.settings
+
+object PreferenceKeys {
+
+ const val APP_THEME = "app_theme"
+ const val APP_VERSION = "app_version"
+ const val AUTOFILL_ENABLE = "autofill_enable"
+ const val BIOMETRIC_AUTH = "biometric_auth"
+ const val CLEAR_CLIPBOARD_20X = "clear_clipboard_20x"
+ const val CLEAR_SAVED_PASS = "clear_saved_pass"
+ const val COPY_ON_DECRYPT = "copy_on_decrypt"
+ const val ENABLE_DEBUG_LOGGING = "enable_debug_logging"
+ const val EXPORT_PASSWORDS = "export_passwords"
+ const val FILTER_RECURSIVELY = "filter_recursively"
+ const val GENERAL_SHOW_TIME = "general_show_time"
+ const val GIT_CONFIG = "git_config"
+ const val GIT_CONFIG_AUTHOR_EMAIL = "git_config_user_email"
+ const val GIT_CONFIG_AUTHOR_NAME = "git_config_user_name"
+ const val GIT_EXTERNAL = "git_external"
+ const val GIT_EXTERNAL_REPO = "git_external_repo"
+ const val GIT_REMOTE_AUTH = "git_remote_auth"
+ const val GIT_REMOTE_KEY_TYPE = "git_remote_key_type"
+
+ @Deprecated("Use GIT_REMOTE_URL instead")
+ const val GIT_REMOTE_LOCATION = "git_remote_location"
+ const val GIT_REMOTE_USE_MULTIPLEXING = "git_remote_use_multiplexing"
+
+ @Deprecated("Use GIT_REMOTE_URL instead")
+ const val GIT_REMOTE_PORT = "git_remote_port"
+
+ @Deprecated("Use GIT_REMOTE_URL instead")
+ const val GIT_REMOTE_PROTOCOL = "git_remote_protocol"
+ const val GIT_DELETE_REPO = "git_delete_repo"
+
+ @Deprecated("Use GIT_REMOTE_URL instead")
+ const val GIT_REMOTE_SERVER = "git_remote_server"
+ const val GIT_REMOTE_URL = "git_remote_url"
+
+ @Deprecated("Use GIT_REMOTE_URL instead")
+ const val GIT_REMOTE_USERNAME = "git_remote_username"
+ const val GIT_SERVER_INFO = "git_server_info"
+ const val GIT_BRANCH_NAME = "git_branch"
+ const val HTTPS_PASSWORD = "https_password"
+ const val LENGTH = "length"
+ const val OREO_AUTOFILL_CUSTOM_PUBLIC_SUFFIXES = "oreo_autofill_custom_public_suffixes"
+ const val OREO_AUTOFILL_DEFAULT_USERNAME = "oreo_autofill_default_username"
+ const val OREO_AUTOFILL_DIRECTORY_STRUCTURE = "oreo_autofill_directory_structure"
+ const val PREF_KEY_CUSTOM_DICT = "pref_key_custom_dict"
+ const val PREF_KEY_IS_CUSTOM_DICT = "pref_key_is_custom_dict"
+ const val PREF_KEY_PWGEN_TYPE = "pref_key_pwgen_type"
+ const val PREF_SELECT_EXTERNAL = "pref_select_external"
+ const val REPOSITORY_INITIALIZED = "repository_initialized"
+ const val REPO_CHANGED = "repo_changed"
+ const val SEARCH_ON_START = "search_on_start"
+ const val SHOW_EXTRA_CONTENT = "show_extra_content"
+
+ @Deprecated(
+ message = "Use SHOW_HIDDEN_CONTENTS instead",
+ replaceWith = ReplaceWith("PreferenceKeys.SHOW_HIDDEN_CONTENTS")
+ )
+ const val SHOW_HIDDEN_FOLDERS = "show_hidden_folders"
+ const val SHOW_HIDDEN_CONTENTS = "show_hidden_contents"
+ const val SORT_ORDER = "sort_order"
+ const val SHOW_PASSWORD = "show_password"
+ const val SSH_KEY = "ssh_key"
+ const val SSH_KEYGEN = "ssh_keygen"
+ const val SSH_KEY_LOCAL_PASSPHRASE = "ssh_key_local_passphrase"
+ const val SSH_OPENKEYSTORE_CLEAR_KEY_ID = "ssh_openkeystore_clear_keyid"
+ const val SSH_OPENKEYSTORE_KEYID = "ssh_openkeystore_keyid"
+ const val SSH_SEE_KEY = "ssh_see_key"
+
+ @Deprecated("To be used only in Migrations.kt")
+ const val USE_GENERATED_KEY = "use_generated_key"
+
+ const val PROXY_SETTINGS = "proxy_settings"
+ const val PROXY_HOST = "proxy_host"
+ const val PROXY_PORT = "proxy_port"
+ const val PROXY_USERNAME = "proxy_username"
+ const val PROXY_PASSWORD = "proxy_password"
+}