aboutsummaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'app/src')
-rw-r--r--app/src/androidTest/java/com/zeapo/pwdstore/MigrationsTest.kt38
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/Application.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/Migrations.kt15
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt22
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt17
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt10
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/UserPreference.kt3
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt5
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt14
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt77
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt8
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt100
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt64
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java383
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt12
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt46
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainKeyProvider.kt212
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt93
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt (renamed from app/src/main/java/com/zeapo/pwdstore/git/config/SshjConfig.kt)6
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjSessionFactory.kt (renamed from app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt)11
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt6
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/BiometricAuthenticator.kt18
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt5
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt61
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt12
-rw-r--r--app/src/main/res/layout/activity_git_clone.xml64
-rw-r--r--app/src/main/res/layout/password_creation_activity.xml183
-rw-r--r--app/src/main/res/values-pt-rBR/strings.xml2
-rw-r--r--app/src/main/res/values-ru/strings.xml2
-rw-r--r--app/src/main/res/values/arrays.xml2
-rw-r--r--app/src/main/res/values/strings.xml8
-rw-r--r--app/src/main/res/xml/oreo_autofill_service.xml2
-rw-r--r--app/src/main/res/xml/preference.xml2
35 files changed, 712 insertions, 797 deletions
diff --git a/app/src/androidTest/java/com/zeapo/pwdstore/MigrationsTest.kt b/app/src/androidTest/java/com/zeapo/pwdstore/MigrationsTest.kt
index d1b04fc3..5ba1b307 100644
--- a/app/src/androidTest/java/com/zeapo/pwdstore/MigrationsTest.kt
+++ b/app/src/androidTest/java/com/zeapo/pwdstore/MigrationsTest.kt
@@ -4,17 +4,20 @@
*/
@file:Suppress("DEPRECATION")
+
package com.zeapo.pwdstore
import android.content.Context
import androidx.core.content.edit
+import com.zeapo.pwdstore.git.config.AuthMode
+import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNull
import org.junit.Test
-import org.junit.Assert.*
-
class MigrationsTest {
private fun checkOldKeysAreRemoved(context: Context) = with(context.sharedPrefs) {
@@ -22,6 +25,7 @@ class MigrationsTest {
assertNull(getString(PreferenceKeys.GIT_REMOTE_USERNAME))
assertNull(getString(PreferenceKeys.GIT_REMOTE_SERVER))
assertNull(getString(PreferenceKeys.GIT_REMOTE_LOCATION))
+ assertNull(getString(PreferenceKeys.GIT_REMOTE_PROTOCOL))
}
@Test
@@ -33,7 +37,8 @@ class MigrationsTest {
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "/mnt/disk3/pass-repo")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "192.168.0.102")
- putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, "ssh://")
+ putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, Protocol.Ssh.pref)
+ putString(PreferenceKeys.GIT_REMOTE_AUTH, AuthMode.Password.pref)
}
runMigrations(context)
checkOldKeysAreRemoved(context)
@@ -51,7 +56,8 @@ class MigrationsTest {
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "/mnt/disk3/pass-repo")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "192.168.0.102")
- putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, "ssh://")
+ putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, Protocol.Ssh.pref)
+ putString(PreferenceKeys.GIT_REMOTE_AUTH, AuthMode.SshKey.pref)
}
runMigrations(context)
checkOldKeysAreRemoved(context)
@@ -69,7 +75,8 @@ class MigrationsTest {
putString(PreferenceKeys.GIT_REMOTE_USERNAME, "msfjarvis")
putString(PreferenceKeys.GIT_REMOTE_LOCATION, "Android-Password-Store/pass-test")
putString(PreferenceKeys.GIT_REMOTE_SERVER, "github.com")
- putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, "https://")
+ putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, Protocol.Https.pref)
+ putString(PreferenceKeys.GIT_REMOTE_AUTH, AuthMode.None.pref)
}
runMigrations(context)
checkOldKeysAreRemoved(context)
@@ -78,4 +85,25 @@ class MigrationsTest {
"https://github.com/Android-Password-Store/pass-test"
)
}
+
+ @Test
+ fun verifyHiddenFoldersMigrationIfDisabled() {
+ val context = Application.instance.applicationContext
+ context.sharedPrefs.edit { clear() }
+ runMigrations(context)
+ assertEquals(true, context.sharedPrefs.getBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, true))
+ assertEquals(false, context.sharedPrefs.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false))
+ }
+
+ @Test
+ fun verifyHiddenFoldersMigrationIfEnabled() {
+ val context = Application.instance.applicationContext
+ context.sharedPrefs.edit {
+ clear()
+ putBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, true)
+ }
+ runMigrations(context)
+ assertEquals(false, context.sharedPrefs.getBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, false))
+ assertEquals(true, context.sharedPrefs.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false))
+ }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/Application.kt b/app/src/main/java/com/zeapo/pwdstore/Application.kt
index 544eb047..3108dee3 100644
--- a/app/src/main/java/com/zeapo/pwdstore/Application.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/Application.kt
@@ -12,7 +12,7 @@ import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_NO
import androidx.appcompat.app.AppCompatDelegate.MODE_NIGHT_YES
import com.github.ajalt.timberkt.Timber.DebugTree
import com.github.ajalt.timberkt.Timber.plant
-import com.zeapo.pwdstore.git.config.setUpBouncyCastleForSshj
+import com.zeapo.pwdstore.git.sshj.setUpBouncyCastleForSshj
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.getString
diff --git a/app/src/main/java/com/zeapo/pwdstore/Migrations.kt b/app/src/main/java/com/zeapo/pwdstore/Migrations.kt
index 1e1943d8..f7cce784 100644
--- a/app/src/main/java/com/zeapo/pwdstore/Migrations.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/Migrations.kt
@@ -19,6 +19,7 @@ import java.net.URI
fun runMigrations(context: Context) {
migrateToGitUrlBasedConfig(context)
+ migrateToHideAll(context)
}
private fun migrateToGitUrlBasedConfig(context: Context) {
@@ -75,9 +76,21 @@ private fun migrateToGitUrlBasedConfig(context: Context) {
remove(PreferenceKeys.GIT_REMOTE_PORT)
remove(PreferenceKeys.GIT_REMOTE_SERVER)
remove(PreferenceKeys.GIT_REMOTE_USERNAME)
+ remove(PreferenceKeys.GIT_REMOTE_PROTOCOL)
}
- if (url == null || !GitSettings.updateUrlIfValid(url)) {
+ 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(context: Context) {
+ context.sharedPrefs.all[PreferenceKeys.SHOW_HIDDEN_FOLDERS] ?: return
+ val isHidden = context.sharedPrefs.getBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, false)
+ context.sharedPrefs.edit {
+ remove(PreferenceKeys.SHOW_HIDDEN_FOLDERS)
+ putBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, isHidden)
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
index 6657afaf..7e0ef418 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
@@ -16,6 +16,7 @@ import android.view.animation.Animation
import android.view.animation.AnimationUtils
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.view.ActionMode
+import androidx.core.content.edit
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.DividerItemDecoration
@@ -25,13 +26,16 @@ import com.zeapo.pwdstore.databinding.PasswordRecyclerViewBinding
import com.zeapo.pwdstore.git.BaseGitActivity
import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
-import com.zeapo.pwdstore.git.config.ConnectionMode
+import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.ui.OnOffItemAnimator
import com.zeapo.pwdstore.ui.adapters.PasswordItemRecyclerAdapter
import com.zeapo.pwdstore.ui.dialogs.ItemCreationBottomSheet
import com.zeapo.pwdstore.utils.PasswordItem
import com.zeapo.pwdstore.utils.PasswordRepository
+import com.zeapo.pwdstore.utils.PreferenceKeys
+import com.zeapo.pwdstore.utils.base64
+import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.sharedPrefs
import com.zeapo.pwdstore.utils.viewBinding
import java.io.File
@@ -89,10 +93,10 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
.show()
binding.swipeRefresher.isRefreshing = false
} else {
- // When authentication is set to ConnectionMode.None then the only git operation we
- // can run is a pull, so automatically fallback to that.
- val operationId = when (GitSettings.connectionMode) {
- ConnectionMode.None -> BaseGitActivity.REQUEST_PULL
+ // When authentication is set to AuthMode.None then the only git operation we can
+ // run is a pull, so automatically fallback to that.
+ val operationId = when (GitSettings.authMode) {
+ AuthMode.None -> BaseGitActivity.REQUEST_PULL
else -> BaseGitActivity.REQUEST_SYNC
}
val intent = Intent(context, GitOperationActivity::class.java)
@@ -243,6 +247,14 @@ class PasswordFragment : Fragment(R.layout.password_recycler_view) {
try {
listener = object : OnFragmentInteractionListener {
override fun onFragmentInteraction(item: PasswordItem) {
+ if (settings.getString(PreferenceKeys.SORT_ORDER) == PasswordRepository.PasswordSortOrder.RECENTLY_USED.name) {
+ //save the time when password was used
+ val preferences = context.getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
+ preferences.edit {
+ putString(item.file.absolutePath.base64(), System.currentTimeMillis().toString())
+ }
+ }
+
if (item.type == PasswordItem.TYPE_CATEGORY) {
navigateTo(item.file)
} else {
diff --git a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
index 3ebd35b8..d9afc7c9 100644
--- a/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt
@@ -6,6 +6,7 @@ package com.zeapo.pwdstore
import android.Manifest
import android.annotation.SuppressLint
+import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ShortcutInfo.Builder
@@ -49,7 +50,7 @@ import com.zeapo.pwdstore.crypto.PasswordCreationActivity
import com.zeapo.pwdstore.git.BaseGitActivity
import com.zeapo.pwdstore.git.GitOperationActivity
import com.zeapo.pwdstore.git.GitServerConfigActivity
-import com.zeapo.pwdstore.git.config.ConnectionMode
+import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.ui.dialogs.FolderCreationDialogFragment
import com.zeapo.pwdstore.utils.PasswordItem
@@ -63,6 +64,7 @@ import com.zeapo.pwdstore.utils.PasswordRepository.Companion.initialize
import com.zeapo.pwdstore.utils.PasswordRepository.Companion.isInitialized
import com.zeapo.pwdstore.utils.PasswordRepository.PasswordSortOrder.Companion.getSortOrder
import com.zeapo.pwdstore.utils.PreferenceKeys
+import com.zeapo.pwdstore.utils.base64
import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.contains
import com.zeapo.pwdstore.utils.getString
@@ -247,7 +249,7 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
val menuRes = when {
- GitSettings.connectionMode == ConnectionMode.None -> R.menu.main_menu_no_auth
+ GitSettings.authMode == AuthMode.None -> R.menu.main_menu_no_auth
PasswordRepository.isGitRepo() -> R.menu.main_menu_git
else -> R.menu.main_menu_non_git
}
@@ -753,6 +755,17 @@ class PasswordStore : AppCompatActivity(R.layout.activity_pwdstore) {
!newCategory.isInsideRepository() -> renameCategory(oldCategory, CategoryRenameError.DestinationOutsideRepo)
else -> lifecycleScope.launch(Dispatchers.IO) {
moveFile(oldCategory.file, newCategory)
+
+ //associate the new category with the last category's timestamp in history
+ val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
+ val timestamp = preference.getString(oldCategory.file.absolutePath.base64())
+ if (timestamp != null) {
+ preference.edit {
+ remove(oldCategory.file.absolutePath.base64())
+ putString(newCategory.absolutePath.base64(), timestamp)
+ }
+ }
+
withContext(Dispatchers.Main) {
commitChange(
resources.getString(R.string.git_commit_move_text, oldCategory.name, newCategory.name),
diff --git a/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt b/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt
index 7ffa71a3..6da5de7a 100644
--- a/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt
@@ -94,8 +94,9 @@ private fun PasswordItem.Companion.makeComparator(
PasswordRepository.PasswordSortOrder.FOLDER_FIRST -> compareBy { it.type }
// In order to let INDEPENDENT not distinguish between items based on their type, we simply
// declare them all equal at this stage.
- PasswordRepository.PasswordSortOrder.INDEPENDENT -> Comparator<PasswordItem> { _, _ -> 0 }
+ PasswordRepository.PasswordSortOrder.INDEPENDENT -> Comparator { _, _ -> 0 }
PasswordRepository.PasswordSortOrder.FILE_FIRST -> compareByDescending { it.type }
+ PasswordRepository.PasswordSortOrder.RECENTLY_USED -> PasswordRepository.PasswordSortOrder.RECENTLY_USED.comparator
}
.then(compareBy(nullsLast(CaseInsensitiveComparator)) {
directoryStructure.getIdentifierFor(it.file)
@@ -139,8 +140,8 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
private val root
get() = PasswordRepository.getRepositoryDirectory()
private val settings by lazy { application.sharedPrefs }
- private val showHiddenDirs
- get() = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, false)
+ private val showHiddenContents
+ get() = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
private val defaultSearchMode
get() = if (settings.getBoolean(PreferenceKeys.FILTER_RECURSIVELY, true)) {
SearchMode.RecursivelyInSubdirectories
@@ -253,8 +254,9 @@ class SearchableRepositoryViewModel(application: Application) : AndroidViewModel
}.asLiveData(Dispatchers.IO)
private fun shouldTake(file: File) = with(file) {
+ if (showHiddenContents) return true
if (isDirectory) {
- !isHidden || showHiddenDirs
+ !isHidden
} else {
!isHidden && file.extension == "gpg"
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
index 3b73d6e5..8f306f6b 100644
--- a/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/UserPreference.kt
@@ -279,7 +279,8 @@ class UserPreference : AppCompatActivity() {
findPreference<CheckBoxPreference>(PreferenceKeys.ENABLE_DEBUG_LOGGING)?.isVisible = !BuildConfig.ENABLE_DEBUG_FEATURES
findPreference<CheckBoxPreference>(PreferenceKeys.BIOMETRIC_AUTH)?.apply {
- val isFingerprintSupported = BiometricManager.from(requireContext()).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
+ val isFingerprintSupported = BiometricManager.from(requireContext())
+ .canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS
if (!isFingerprintSupported) {
isEnabled = false
isChecked = false
diff --git a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt
index 5c8b6b3b..38bdd068 100644
--- a/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/autofill/oreo/FeatureAndTrustDetection.kt
@@ -65,6 +65,7 @@ private val TRUSTED_BROWSER_CERTIFICATE_HASH = mapOf(
"com.opera.mini.native" to "V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I=",
"com.opera.mini.native.beta" to "V6y8Ul8bLr0ZGWzW8BQ5fMkQ/RiEHgroUP68Ph5ZP/I=",
"com.opera.touch" to "qtjiBNJNF3k0yc0MY8xqo4779CxKaVcJfiIQ9X+qZ6o=",
+ "org.bromite.bromite" to "4e5c0HbXsNyEyytF+3i4bfLrOaO2xWuj3CkqXgw7lQQ=",
"org.gnu.icecat" to "wi2iuVvK/WYZUzd2g0Qzn9ef3kAisQURZ8U1WSMTkcM=",
"org.mozilla.fenix" to "UAR3kIjn+YjVvFzF+HmP6/T4zQhKGypG79TI7krq8hE=",
"org.mozilla.fenix.nightly" to "d+rEzu02r++6dheZMd1MwZWrDNVLrzVdIV57vdKOQCo=",
@@ -75,6 +76,8 @@ private val TRUSTED_BROWSER_CERTIFICATE_HASH = mapOf(
"org.mozilla.focus" to "YgOkc7421k7jf4f6UA7bx56rkwYQq5ufpMp9XB8bT/w=",
"org.mozilla.klar" to "YgOkc7421k7jf4f6UA7bx56rkwYQq5ufpMp9XB8bT/w=",
"org.torproject.torbrowser" to "IAYfBF5zfGc3XBd5TP7bQ2oDzsa6y3y5+WZCIFyizsg=",
+ "org.ungoogled.chromium.stable" to "29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk=",
+ "org.ungoogled.chromium.extensions.stable" to "29UOO5cXoxO/e/hH3hOu6bbtg1My4tK6Eik2Ym5Krtk=",
)
private fun isTrustedBrowser(context: Context, appPackage: String): Boolean {
@@ -163,6 +166,8 @@ private val FLAKY_BROWSERS = listOf(
"com.chrome.beta",
"com.chrome.canary",
"com.chrome.dev",
+ "org.bromite.bromite",
+ "org.ungoogled.chromium.stable",
)
enum class BrowserAutofillSupportLevel {
diff --git a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
index 44376c0a..ff5a8179 100644
--- a/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/crypto/PasswordCreationActivity.kt
@@ -5,6 +5,7 @@
package com.zeapo.pwdstore.crypto
+import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.text.InputType
@@ -14,6 +15,7 @@ import android.view.View
import androidx.activity.result.IntentSenderRequest
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.activity.result.contract.ActivityResultContracts.StartIntentSenderForResult
+import androidx.core.content.edit
import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged
import androidx.lifecycle.lifecycleScope
@@ -30,6 +32,7 @@ import com.zeapo.pwdstore.ui.dialogs.PasswordGeneratorDialogFragment
import com.zeapo.pwdstore.ui.dialogs.XkPasswordGeneratorDialogFragment
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
+import com.zeapo.pwdstore.utils.base64
import com.zeapo.pwdstore.utils.commitChange
import com.zeapo.pwdstore.utils.getString
import com.zeapo.pwdstore.utils.isInsideRepository
@@ -411,6 +414,17 @@ class PasswordCreationActivity : BasePgpActivity(), OpenPgpServiceConnection.OnB
return@executeApiAsync
}
+ //associate the new password name with the last name's timestamp in history
+ val preference = getSharedPreferences("recent_password_history", Context.MODE_PRIVATE)
+ val oldFilePathHash = "$repoPath/${oldCategory?.trim('/')}/$oldFileName.gpg".base64()
+ val timestamp = preference.getString(oldFilePathHash)
+ if (timestamp != null) {
+ preference.edit {
+ remove(oldFilePathHash)
+ putString(file.absolutePath.base64(), timestamp)
+ }
+ }
+
val returnIntent = Intent()
returnIntent.putExtra(RETURN_EXTRA_CREATED_FILE, path)
returnIntent.putExtra(RETURN_EXTRA_NAME, editName)
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
index fb0831d6..ed72c88d 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/BaseGitActivity.kt
@@ -4,17 +4,12 @@
*/
package com.zeapo.pwdstore.git
-import android.content.Intent
import android.view.MenuItem
-import androidx.annotation.CallSuper
import androidx.appcompat.app.AppCompatActivity
-import androidx.lifecycle.lifecycleScope
import com.github.ajalt.timberkt.Timber.tag
import com.github.ajalt.timberkt.e
import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.zeapo.pwdstore.git.config.ConnectionMode
import com.zeapo.pwdstore.git.config.GitSettings
-import com.zeapo.pwdstore.git.config.SshApiSessionFactory
import com.zeapo.pwdstore.git.operation.BreakOutOfDetached
import com.zeapo.pwdstore.git.operation.CloneOperation
import com.zeapo.pwdstore.git.operation.GitOperation
@@ -23,7 +18,6 @@ import com.zeapo.pwdstore.git.operation.PushOperation
import com.zeapo.pwdstore.git.operation.ResetToRemoteOperation
import com.zeapo.pwdstore.git.operation.SyncOperation
import com.zeapo.pwdstore.utils.PasswordRepository
-import kotlinx.coroutines.launch
/**
* Abstract AppCompatActivity that holds some information that is commonly shared across git-related
@@ -31,9 +25,6 @@ import kotlinx.coroutines.launch
*/
abstract class BaseGitActivity : AppCompatActivity() {
- private var identityBuilder: SshApiSessionFactory.IdentityBuilder? = null
- private var identity: SshApiSessionFactory.ApiIdentity? = null
-
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
android.R.id.home -> {
@@ -44,23 +35,8 @@ abstract class BaseGitActivity : AppCompatActivity() {
}
}
- @CallSuper
- override fun onDestroy() {
- // Do not leak the service connection
- if (identityBuilder != null) {
- identityBuilder!!.close()
- identityBuilder = null
- }
- super.onDestroy()
- }
-
/**
- * Attempt to launch the requested Git operation. Depending on the configured auth, it may not
- * be possible to launch the operation immediately. In that case, this function may launch an
- * intermediate activity instead, which will gather necessary information and post it back via
- * onActivityResult, which will then re-call this function. This may happen multiple times,
- * until either an error is encountered or the operation is successfully launched.
- *
+ * Attempt to launch the requested Git operation.
* @param operation The type of git operation to launch
*/
suspend fun launchGitOperation(operation: Int) {
@@ -70,21 +46,6 @@ abstract class BaseGitActivity : AppCompatActivity() {
return
}
try {
- // Before launching the operation with OpenKeychain auth, we need to issue several requests
- // to the OpenKeychain API. IdentityBuild will take care of launching the relevant intents,
- // we just need to keep calling it until it returns a completed ApiIdentity.
- if (GitSettings.connectionMode == ConnectionMode.OpenKeychain && identity == null) {
- // Lazy initialization of the IdentityBuilder
- if (identityBuilder == null) {
- identityBuilder = SshApiSessionFactory.IdentityBuilder(this)
- }
- // Try to get an ApiIdentity and bail if one is not ready yet. The builder will ensure
- // that onActivityResult is called with operation again, which will re-invoke us here
- identity = identityBuilder!!.tryBuild(operation)
- if (identity == null)
- return
- }
-
val localDir = requireNotNull(PasswordRepository.getRepositoryDirectory())
val op = when (operation) {
REQUEST_CLONE, GitOperation.GET_SSH_KEY_FROM_CLONE -> CloneOperation(localDir, GitSettings.url!!, this)
@@ -93,7 +54,6 @@ abstract class BaseGitActivity : AppCompatActivity() {
REQUEST_SYNC -> SyncOperation(localDir, this)
BREAK_OUT_OF_DETACHED -> BreakOutOfDetached(localDir, this)
REQUEST_RESET -> ResetToRemoteOperation(localDir, this)
- SshApiSessionFactory.POST_SIGNATURE -> return
else -> {
tag(TAG).e { "Operation not recognized : $operation" }
setResult(RESULT_CANCELED)
@@ -101,46 +61,13 @@ abstract class BaseGitActivity : AppCompatActivity() {
return
}
}
- op.executeAfterAuthentication(GitSettings.connectionMode, identity)
+ op.executeAfterAuthentication(GitSettings.authMode)
} catch (e: Exception) {
e.printStackTrace()
MaterialAlertDialogBuilder(this).setMessage(e.message).show()
}
}
- public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
- // In addition to the pre-operation-launch series of intents for OpenKeychain auth
- // that will pass through here and back to launchGitOperation, there is one
- // synchronous operation that happens /after/ the operation has been launched in the
- // background thread - the actual signing of the SSH challenge. We pass through the
- // completed signature to the ApiIdentity, which will be blocked in the other thread
- // waiting for it.
- if (requestCode == SshApiSessionFactory.POST_SIGNATURE && identity != null) {
- identity!!.postSignature(data)
-
- // If the signature failed (usually because it was cancelled), reset state
- if (data == null) {
- identity = null
- identityBuilder = null
- }
- return
- }
-
- if (resultCode == RESULT_CANCELED) {
- setResult(RESULT_CANCELED)
- finish()
- } else if (resultCode == RESULT_OK) {
- // If an operation has been re-queued via this mechanism, let the
- // IdentityBuilder attempt to extract some updated state from the intent before
- // trying to re-launch the operation.
- if (identityBuilder != null) {
- identityBuilder!!.consume(data)
- }
- lifecycleScope.launch { launchGitOperation(requestCode) }
- }
- super.onActivityResult(requestCode, resultCode, data)
- }
-
companion object {
const val REQUEST_ARG_OP = "OPERATION"
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt
index f06fc891..bf35e5c7 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitCommandExecutor.kt
@@ -14,7 +14,8 @@ import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.git.GitException.PullException
import com.zeapo.pwdstore.git.GitException.PushException
-import com.zeapo.pwdstore.git.config.SshjSessionFactory
+import com.zeapo.pwdstore.git.config.GitSettings
+import com.zeapo.pwdstore.git.sshj.SshjSessionFactory
import com.zeapo.pwdstore.git.operation.GitOperation
import com.zeapo.pwdstore.utils.Result
import com.zeapo.pwdstore.utils.snackbar
@@ -28,6 +29,7 @@ import org.eclipse.jgit.api.PullCommand
import org.eclipse.jgit.api.PushCommand
import org.eclipse.jgit.api.RebaseResult
import org.eclipse.jgit.api.StatusCommand
+import org.eclipse.jgit.lib.PersonIdent
import org.eclipse.jgit.transport.RemoteRefUpdate
import org.eclipse.jgit.transport.SshSessionFactory
@@ -60,7 +62,9 @@ class GitCommandExecutor(
// the previous status will eventually be used to avoid a commit
if (nbChanges > 0) {
withContext(Dispatchers.IO) {
- command.call()
+ command
+ .setAuthor(PersonIdent(GitSettings.authorName, GitSettings.authorEmail))
+ .call()
}
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt
index 5138b50b..9205e7fc 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitConfigActivity.kt
@@ -61,8 +61,6 @@ class GitConfigActivity : BaseGitActivity() {
} else {
GitSettings.authorEmail = email
GitSettings.authorName = name
- PasswordRepository.setGitAuthorEmail(email)
- PasswordRepository.setGitAuthorName(name)
Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show()
Handler().postDelayed(500) { finish() }
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
index 82e97e17..5aa34201 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/GitServerConfigActivity.kt
@@ -13,7 +13,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.databinding.ActivityGitCloneBinding
-import com.zeapo.pwdstore.git.config.ConnectionMode
+import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings
import com.zeapo.pwdstore.git.config.Protocol
import com.zeapo.pwdstore.utils.PasswordRepository
@@ -32,6 +32,8 @@ class GitServerConfigActivity : BaseGitActivity() {
private val binding by viewBinding(ActivityGitCloneBinding::inflate)
+ private lateinit var newAuthMode: AuthMode
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val isClone = intent?.extras?.getInt(REQUEST_ARG_OP) ?: -1 == REQUEST_CLONE
@@ -41,78 +43,64 @@ class GitServerConfigActivity : BaseGitActivity() {
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
- binding.cloneProtocolGroup.check(when (GitSettings.protocol) {
- Protocol.Ssh -> R.id.clone_protocol_ssh
- Protocol.Https -> R.id.clone_protocol_https
- })
- binding.cloneProtocolGroup.addOnButtonCheckedListener { _, checkedId, checked ->
- if (checked) {
- when (checkedId) {
- R.id.clone_protocol_https -> GitSettings.protocol = Protocol.Https
- R.id.clone_protocol_ssh -> GitSettings.protocol = Protocol.Ssh
- }
- updateConnectionModeToggleGroup()
- }
- }
+ newAuthMode = GitSettings.authMode
- binding.connectionModeGroup.apply {
- when (GitSettings.connectionMode) {
- ConnectionMode.SshKey -> check(R.id.connection_mode_ssh_key)
- ConnectionMode.Password -> check(R.id.connection_mode_password)
- ConnectionMode.OpenKeychain -> check(R.id.connection_mode_open_keychain)
- ConnectionMode.None -> uncheck(checkedButtonId)
+ binding.authModeGroup.apply {
+ when (newAuthMode) {
+ AuthMode.SshKey -> check(R.id.auth_mode_ssh_key)
+ AuthMode.Password -> check(R.id.auth_mode_password)
+ AuthMode.OpenKeychain -> check(R.id.auth_mode_open_keychain)
+ AuthMode.None -> uncheck(checkedButtonId)
}
addOnButtonCheckedListener { _, _, _ ->
when (checkedButtonId) {
- R.id.connection_mode_ssh_key -> GitSettings.connectionMode = ConnectionMode.SshKey
- R.id.connection_mode_open_keychain -> GitSettings.connectionMode = ConnectionMode.OpenKeychain
- R.id.connection_mode_password -> GitSettings.connectionMode = ConnectionMode.Password
- View.NO_ID -> GitSettings.connectionMode = ConnectionMode.None
+ R.id.auth_mode_ssh_key -> newAuthMode = AuthMode.SshKey
+ R.id.auth_mode_open_keychain -> newAuthMode = AuthMode.OpenKeychain
+ R.id.auth_mode_password -> newAuthMode = AuthMode.Password
+ View.NO_ID -> newAuthMode = AuthMode.None
}
}
}
- updateConnectionModeToggleGroup()
binding.serverUrl.setText(GitSettings.url)
binding.serverBranch.setText(GitSettings.branch)
binding.saveButton.setOnClickListener {
- if (isClone && PasswordRepository.getRepository(null) == null)
- PasswordRepository.initialize()
- GitSettings.branch = binding.serverBranch.text.toString().trim()
- if (GitSettings.updateUrlIfValid(binding.serverUrl.text.toString().trim())) {
- if (!isClone) {
- Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show()
- Handler().postDelayed(500) { finish() }
- } else {
- cloneRepository()
+ when (val updateResult = GitSettings.updateConnectionSettingsIfValid(
+ newAuthMode = newAuthMode,
+ newUrl = binding.serverUrl.text.toString().trim(),
+ newBranch = binding.serverBranch.text.toString().trim())) {
+ GitSettings.UpdateConnectionSettingsResult.FailedToParseUrl -> {
+ Snackbar.make(binding.root, getString(R.string.git_server_config_save_error), Snackbar.LENGTH_LONG).show()
+ }
+ is GitSettings.UpdateConnectionSettingsResult.MissingUsername -> {
+ when (updateResult.newProtocol) {
+ Protocol.Https -> Snackbar.make(binding.root, getString(R.string.git_server_config_save_missing_username_https), Snackbar.LENGTH_LONG).show()
+ Protocol.Ssh -> Snackbar.make(binding.root, getString(R.string.git_server_config_save_missing_username_ssh), Snackbar.LENGTH_LONG).show()
+ }
+ }
+ GitSettings.UpdateConnectionSettingsResult.Valid -> {
+ if (isClone && PasswordRepository.getRepository(null) == null)
+ PasswordRepository.initialize()
+ if (!isClone) {
+ Snackbar.make(binding.root, getString(R.string.git_server_config_save_success), Snackbar.LENGTH_SHORT).show()
+ Handler().postDelayed(500) { finish() }
+ } else {
+ cloneRepository()
+ }
+ }
+ is GitSettings.UpdateConnectionSettingsResult.AuthModeMismatch -> {
+ val message = getString(
+ R.string.git_server_config_save_auth_mode_mismatch,
+ updateResult.newProtocol,
+ updateResult.validModes.joinToString(", "),
+ )
+ Snackbar.make(binding.root, message, Snackbar.LENGTH_LONG).show()
}
- } else {
- Snackbar.make(binding.root, getString(R.string.git_server_config_save_error), Snackbar.LENGTH_LONG).show()
}
}
}
- private fun updateConnectionModeToggleGroup() {
- if (GitSettings.protocol == Protocol.Ssh) {
- // Reset connection mode to SSH key if the current value (none) is not valid for SSH
- if (binding.connectionModeGroup.checkedButtonIds.isEmpty())
- binding.connectionModeGroup.check(R.id.connection_mode_ssh_key)
- binding.connectionModeSshKey.isEnabled = true
- binding.connectionModeOpenKeychain.isEnabled = true
- binding.connectionModeGroup.isSelectionRequired = true
- } else {
- binding.connectionModeGroup.isSelectionRequired = false
- // Reset connection mode to password if the current value is not valid for HTTPS
- // Important note: This has to happen before disabling the other toggle buttons or they
- // won't uncheck.
- if (GitSettings.connectionMode !in listOf(ConnectionMode.None, ConnectionMode.Password))
- binding.connectionModeGroup.check(R.id.connection_mode_password)
- binding.connectionModeSshKey.isEnabled = false
- binding.connectionModeOpenKeychain.isEnabled = false
- }
- }
-
/**
* Clones the repository, the directory exists, deletes it
*/
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt
index b81a8f8f..b0d931e0 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/config/GitSettings.kt
@@ -29,7 +29,7 @@ enum class Protocol(val pref: String) {
}
}
-enum class ConnectionMode(val pref: String) {
+enum class AuthMode(val pref: String) {
SshKey("ssh-key"),
Password("username/password"),
OpenKeychain("OpenKeychain"),
@@ -38,10 +38,10 @@ enum class ConnectionMode(val pref: String) {
companion object {
- private val map = values().associateBy(ConnectionMode::pref)
- fun fromString(type: String?): ConnectionMode {
+ private val map = values().associateBy(AuthMode::pref)
+ fun fromString(type: String?): AuthMode {
return map[type ?: return SshKey]
- ?: throw IllegalArgumentException("$type is not a valid ConnectionMode")
+ ?: throw IllegalArgumentException("$type is not a valid AuthMode")
}
}
}
@@ -53,16 +53,9 @@ object GitSettings {
private val settings by lazy { Application.instance.sharedPrefs }
private val encryptedSettings by lazy { Application.instance.getEncryptedPrefs("git_operation") }
- var protocol
- get() = Protocol.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_PROTOCOL))
- set(value) {
- settings.edit {
- putString(PreferenceKeys.GIT_REMOTE_PROTOCOL, value.pref)
- }
- }
- var connectionMode
- get() = ConnectionMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH))
- set(value) {
+ var authMode
+ get() = AuthMode.fromString(settings.getString(PreferenceKeys.GIT_REMOTE_AUTH))
+ private set(value) {
settings.edit {
putString(PreferenceKeys.GIT_REMOTE_AUTH, value.pref)
}
@@ -71,6 +64,8 @@ object GitSettings {
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)
}
@@ -96,20 +91,47 @@ object GitSettings {
}
var branch
get() = settings.getString(PreferenceKeys.GIT_BRANCH_NAME) ?: DEFAULT_BRANCH
- set(value) {
+ private set(value) {
settings.edit {
putString(PreferenceKeys.GIT_BRANCH_NAME, value)
}
}
- fun updateUrlIfValid(newUrl: String): Boolean {
- try {
+ 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 = try {
URIish(newUrl)
} catch (_: Exception) {
- return false
+ 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)
+ }
}
- if (newUrl != url)
- url = newUrl
- return true
+
+ url = newUrl
+ authMode = newAuthMode
+ branch = newBranch
+ return UpdateConnectionSettingsResult.Valid
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java b/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java
deleted file mode 100644
index 03760741..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshApiSessionFactory.java
+++ /dev/null
@@ -1,383 +0,0 @@
-/*
- * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
- * SPDX-License-Identifier: GPL-3.0-only
- */
-package com.zeapo.pwdstore.git.config;
-
-import android.app.PendingIntent;
-import android.content.Intent;
-import android.content.IntentSender;
-import android.content.SharedPreferences;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.preference.PreferenceManager;
-
-import com.google.android.material.dialog.MaterialAlertDialogBuilder;
-import com.jcraft.jsch.Identity;
-import com.jcraft.jsch.JSch;
-import com.jcraft.jsch.JSchException;
-import com.jcraft.jsch.Session;
-import com.zeapo.pwdstore.R;
-import com.zeapo.pwdstore.git.BaseGitActivity;
-import com.zeapo.pwdstore.utils.PreferenceKeys;
-
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig;
-import org.eclipse.jgit.util.Base64;
-import org.eclipse.jgit.util.FS;
-import org.openintents.ssh.authentication.ISshAuthenticationService;
-import org.openintents.ssh.authentication.SshAuthenticationApi;
-import org.openintents.ssh.authentication.SshAuthenticationApiError;
-import org.openintents.ssh.authentication.SshAuthenticationConnection;
-import org.openintents.ssh.authentication.request.KeySelectionRequest;
-import org.openintents.ssh.authentication.request.Request;
-import org.openintents.ssh.authentication.request.SigningRequest;
-import org.openintents.ssh.authentication.request.SshPublicKeyRequest;
-import org.openintents.ssh.authentication.util.SshAuthenticationApiUtils;
-
-import java.util.List;
-import java.util.concurrent.CountDownLatch;
-
-public class SshApiSessionFactory extends JschConfigSessionFactory {
- /**
- * Intent request code indicating a completed signature that should be posted to an outstanding
- * ApiIdentity
- */
- public static final int POST_SIGNATURE = 301;
-
- private final Identity identity;
-
- public SshApiSessionFactory(Identity identity) {
- this.identity = identity;
- }
-
- @NonNull
- @Override
- protected JSch getJSch(@NonNull final OpenSshConfig.Host hc, @NonNull FS fs)
- throws JSchException {
- JSch jsch = super.getJSch(hc, fs);
- jsch.removeAllIdentity();
- jsch.addIdentity(identity, null);
- return jsch;
- }
-
- @Override
- protected void configure(@NonNull OpenSshConfig.Host hc, Session session) {
- session.setConfig("StrictHostKeyChecking", "no");
- session.setConfig("PreferredAuthentications", "publickey");
- }
-
- /**
- * Helper to build up an ApiIdentity via the invocation of several pending intents that
- * communicate with OpenKeychain. The user of this class must handle onActivityResult and keep
- * feeding the resulting intents into the IdentityBuilder until it can successfully complete the
- * build.
- */
- public static class IdentityBuilder {
- private final SshAuthenticationConnection connection;
- private final BaseGitActivity callingActivity;
- private final SharedPreferences settings;
- private SshAuthenticationApi api;
- private String keyId, description, alg;
- private byte[] publicKey;
-
- /**
- * Construct a new IdentityBuilder
- *
- * @param callingActivity Activity that will be used to launch pending intents and that will
- * receive and handle the results.
- */
- public IdentityBuilder(BaseGitActivity callingActivity) {
- this.callingActivity = callingActivity;
-
- List<String> providers =
- SshAuthenticationApiUtils.getAuthenticationProviderPackageNames(
- callingActivity);
- if (providers.isEmpty())
- throw new RuntimeException(callingActivity.getString(R.string.no_ssh_api_provider));
-
- // TODO: Handle multiple available providers? Are there actually any in practice beyond
- // OpenKeychain?
- connection = new SshAuthenticationConnection(callingActivity, providers.get(0));
-
- settings =
- PreferenceManager.getDefaultSharedPreferences(
- callingActivity.getApplicationContext());
- keyId = settings.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null);
- }
-
- /**
- * Free any resources associated with this IdentityBuilder
- */
- public void close() {
- if (connection != null && connection.isConnected()) connection.disconnect();
- }
-
- /**
- * Helper to invoke an OpenKeyshain SSH API method and correctly interpret the result.
- *
- * @param request The request intent to launch
- * @param requestCode The request code to use if a pending intent needs to be sent
- * @return The resulting intent if the request completed immediately, or null if we had to
- * launch a pending intent to interact with the user
- */
- private Intent executeApi(Request request, int requestCode) {
- Intent result = api.executeApi(request.toIntent());
-
- switch (result.getIntExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, -1)) {
- case SshAuthenticationApi.RESULT_CODE_ERROR:
- SshAuthenticationApiError error =
- result.getParcelableExtra(SshAuthenticationApi.EXTRA_ERROR);
- // On an OpenKeychain SSH API error, clear out the stored keyid
- settings.edit().putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null).apply();
-
- switch (error.getError()) {
- // If the problem was just a bad keyid, reset to allow them to choose a
- // different one
- case (SshAuthenticationApiError.NO_SUCH_KEY):
- case (SshAuthenticationApiError.NO_AUTH_KEY):
- keyId = null;
- publicKey = null;
- description = null;
- alg = null;
- return executeApi(new KeySelectionRequest(), requestCode);
-
- // Other errors are fatal
- default:
- throw new RuntimeException(error.getMessage());
- }
- case SshAuthenticationApi.RESULT_CODE_SUCCESS:
- break;
- case SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
- PendingIntent pendingIntent =
- result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT);
- try {
- callingActivity.startIntentSenderForResult(
- pendingIntent.getIntentSender(), requestCode, null, 0, 0, 0);
- return null;
- } catch (IntentSender.SendIntentException e) {
- e.printStackTrace();
- throw new RuntimeException(
- callingActivity.getString(R.string.ssh_api_pending_intent_failed));
- }
- default:
- throw new RuntimeException(
- callingActivity.getString(R.string.ssh_api_unknown_error));
- }
-
- return result;
- }
-
- /**
- * Parse a given intent to see if it is the result of an OpenKeychain pending intent. If so,
- * extract any updated state from it.
- *
- * @param intent The intent to inspect
- */
- public void consume(Intent intent) {
- if (intent == null) return;
-
- if (intent.hasExtra(SshAuthenticationApi.EXTRA_KEY_ID)) {
- keyId = intent.getStringExtra(SshAuthenticationApi.EXTRA_KEY_ID);
- description = intent.getStringExtra(SshAuthenticationApi.EXTRA_KEY_DESCRIPTION);
- settings.edit().putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, keyId).apply();
- }
-
- if (intent.hasExtra(SshAuthenticationApi.EXTRA_SSH_PUBLIC_KEY)) {
- String keyStr = intent.getStringExtra(SshAuthenticationApi.EXTRA_SSH_PUBLIC_KEY);
- String[] keyParts = keyStr.split(" ");
- alg = keyParts[0];
- publicKey = Base64.decode(keyParts[1]);
- }
- }
-
- /**
- * Try to build an ApiIdentity that will perform SSH authentication via OpenKeychain.
- *
- * @param requestCode The request code to use if a pending intent needs to be sent
- * @return The built identity, or null of user interaction is still required (in which case
- * a pending intent will have already been launched)
- */
- public ApiIdentity tryBuild(int requestCode) {
- // First gate, need to initiate a connection to the service and wait for it to connect.
- if (api == null) {
- connection.connect(
- new SshAuthenticationConnection.OnBound() {
- @Override
- public void onBound(ISshAuthenticationService sshAgent) {
- api = new SshAuthenticationApi(callingActivity, sshAgent);
- // We can immediately try the next phase without needing to post
- // back
- // though onActivityResult
- callingActivity.onActivityResult(
- requestCode, AppCompatActivity.RESULT_OK, null);
- }
-
- @Override
- public void onError() {
- new MaterialAlertDialogBuilder(callingActivity)
- .setMessage(
- callingActivity.getString(
- R.string.openkeychain_ssh_api_connect_fail))
- .show();
- }
- });
-
- return null;
- }
-
- // Second gate, need the user to select which key they want to use
- if (keyId == null) {
- consume(executeApi(new KeySelectionRequest(), requestCode));
- // If we did not immediately get the result, bail for now and wait to be re-entered
- if (keyId == null) return null;
- }
-
- // Third gate, need to get the public key for the selected key. This one often does not
- // need use interaction.
- if (publicKey == null) {
- consume(executeApi(new SshPublicKeyRequest(keyId), requestCode));
- // If we did not immediately get the result, bail for now and wait to be re-entered
- if (publicKey == null) return null;
- }
-
- // Have everything we need for now, build the identify
- return new ApiIdentity(keyId, description, publicKey, alg, callingActivity, api);
- }
- }
-
- /**
- * A Jsch identity that delegates key operations via the OpenKeychain SSH API
- */
- public static class ApiIdentity implements Identity {
- private final String keyId;
- private final String description;
- private final String alg;
- private final byte[] publicKey;
- private final AppCompatActivity callingActivity;
- private final SshAuthenticationApi api;
- private CountDownLatch latch;
- private byte[] signature;
-
- ApiIdentity(
- String keyId,
- String description,
- byte[] publicKey,
- String alg,
- AppCompatActivity callingActivity,
- SshAuthenticationApi api) {
- this.keyId = keyId;
- this.description = description;
- this.publicKey = publicKey;
- this.alg = alg;
- this.callingActivity = callingActivity;
- this.api = api;
- }
-
- @Override
- public boolean setPassphrase(byte[] passphrase) {
- // We are not encrypted with a passphrase
- return true;
- }
-
- @Override
- public byte[] getPublicKeyBlob() {
- return publicKey;
- }
-
- /**
- * Helper to handle the result of an OpenKeyshain SSH API signing request
- *
- * @param result The result intent to handle
- * @return The signed challenge, or null if it was not immediately available, in which case
- * the latch has been initialized and the pending intent started
- */
- private byte[] handleSignResult(Intent result) {
- switch (result.getIntExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, -1)) {
- case SshAuthenticationApi.RESULT_CODE_ERROR:
- SshAuthenticationApiError error =
- result.getParcelableExtra(SshAuthenticationApi.EXTRA_ERROR);
- throw new RuntimeException(error.getMessage());
- case SshAuthenticationApi.RESULT_CODE_SUCCESS:
- return result.getByteArrayExtra(SshAuthenticationApi.EXTRA_SIGNATURE);
- case SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED:
- PendingIntent pendingIntent =
- result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT);
- try {
- latch = new CountDownLatch(1);
- callingActivity.startIntentSenderForResult(
- pendingIntent.getIntentSender(), POST_SIGNATURE, null, 0, 0, 0);
- return null;
-
- } catch (Exception e) {
- e.printStackTrace();
- throw new RuntimeException(
- callingActivity.getString(R.string.ssh_api_pending_intent_failed));
- }
- default:
- if (result.hasExtra(SshAuthenticationApi.EXTRA_CHALLENGE))
- return handleSignResult(api.executeApi(result));
- throw new RuntimeException(
- callingActivity.getString(R.string.ssh_api_unknown_error));
- }
- }
-
- @Override
- public byte[] getSignature(byte[] data) {
- Intent request = new SigningRequest(data, keyId, SshAuthenticationApi.SHA1).toIntent();
- signature = handleSignResult(api.executeApi(request));
-
- // If we did not immediately get a signature (probable), we will block on a latch until
- // the main activity gets the intent result and posts to us.
- if (signature == null) {
- try {
- latch.await();
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
-
- return signature;
- }
-
- /**
- * Post a signature response back to an in-progress operation using this ApiIdentity.
- *
- * @param data The signature data (hopefully)
- */
- public void postSignature(Intent data) {
- try {
- if (data != null) {
- signature = handleSignResult(data);
- }
- } finally {
- if (latch != null) latch.countDown();
- }
- }
-
- @Override
- public boolean decrypt() {
- return true;
- }
-
- @Override
- public String getAlgName() {
- return alg;
- }
-
- @Override
- public String getName() {
- return description;
- }
-
- @Override
- public boolean isEncrypted() {
- return false;
- }
-
- @Override
- public void clear() {
- }
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt
index 423ceb80..b7b4f881 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt
@@ -10,8 +10,8 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout
import com.zeapo.pwdstore.R
-import com.zeapo.pwdstore.git.config.ConnectionMode
-import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
+import com.zeapo.pwdstore.git.config.AuthMode
+import com.zeapo.pwdstore.git.sshj.InteractivePasswordFinder
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.requestInputFocusOnView
@@ -20,7 +20,7 @@ import kotlin.coroutines.resume
class CredentialFinder(
val callingActivity: FragmentActivity,
- val connectionMode: ConnectionMode
+ val authMode: AuthMode
) : InteractivePasswordFinder() {
override fun askForPassword(cont: Continuation<String?>, isRetry: Boolean) {
@@ -30,15 +30,15 @@ class CredentialFinder(
@StringRes val hintRes: Int
@StringRes val rememberRes: Int
@StringRes val errorRes: Int
- when (connectionMode) {
- ConnectionMode.SshKey -> {
+ when (authMode) {
+ AuthMode.SshKey -> {
credentialPref = PreferenceKeys.SSH_KEY_LOCAL_PASSPHRASE
messageRes = R.string.passphrase_dialog_text
hintRes = R.string.ssh_keygen_passphrase
rememberRes = R.string.git_operation_remember_passphrase
errorRes = R.string.git_operation_wrong_passphrase
}
- ConnectionMode.Password -> {
+ AuthMode.Password -> {
// Could be either an SSH or an HTTPS password
credentialPref = PreferenceKeys.HTTPS_PASSWORD
messageRes = R.string.password_dialog_text
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt
index dc768a6e..806bbb7c 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt
@@ -8,21 +8,20 @@ import android.content.Intent
import androidx.annotation.CallSuper
import androidx.core.content.edit
import androidx.fragment.app.FragmentActivity
-import androidx.preference.PreferenceManager
import com.github.ajalt.timberkt.Timber.d
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.zeapo.pwdstore.R
import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.ErrorMessages
-import com.zeapo.pwdstore.git.config.ConnectionMode
+import com.zeapo.pwdstore.git.config.AuthMode
import com.zeapo.pwdstore.git.config.GitSettings
-import com.zeapo.pwdstore.git.config.InteractivePasswordFinder
-import com.zeapo.pwdstore.git.config.SshApiSessionFactory
-import com.zeapo.pwdstore.git.config.SshAuthData
-import com.zeapo.pwdstore.git.config.SshjSessionFactory
+import com.zeapo.pwdstore.git.sshj.InteractivePasswordFinder
+import com.zeapo.pwdstore.git.sshj.SshAuthData
+import com.zeapo.pwdstore.git.sshj.SshjSessionFactory
import com.zeapo.pwdstore.utils.PasswordRepository
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
+import com.zeapo.pwdstore.utils.sharedPrefs
import java.io.File
import net.schmizz.sshj.userauth.password.PasswordFinder
import org.eclipse.jgit.api.Git
@@ -96,8 +95,9 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
return this
}
- private fun withOpenKeychainAuthentication(identity: SshApiSessionFactory.ApiIdentity?): GitOperation {
- SshSessionFactory.setInstance(SshApiSessionFactory(identity))
+ private fun withOpenKeychainAuthentication(activity: FragmentActivity): GitOperation {
+ val sessionFactory = SshjSessionFactory(SshAuthData.OpenKeychain(activity), hostKeyFile)
+ SshSessionFactory.setInstance(sessionFactory)
this.provider = null
return this
}
@@ -127,11 +127,10 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
abstract suspend fun execute()
suspend fun executeAfterAuthentication(
- connectionMode: ConnectionMode,
- identity: SshApiSessionFactory.ApiIdentity?
+ authMode: AuthMode,
) {
- when (connectionMode) {
- ConnectionMode.SshKey -> if (!sshKeyFile.exists()) {
+ when (authMode) {
+ AuthMode.SshKey -> if (!sshKeyFile.exists()) {
MaterialAlertDialogBuilder(callingActivity)
.setMessage(callingActivity.resources.getString(R.string.ssh_preferences_dialog_text))
.setTitle(callingActivity.resources.getString(R.string.ssh_preferences_dialog_title))
@@ -147,12 +146,12 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
}.show()
} else {
withPublicKeyAuthentication(
- CredentialFinder(callingActivity, connectionMode)).execute()
+ CredentialFinder(callingActivity, authMode)).execute()
}
- ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(identity).execute()
- ConnectionMode.Password -> withPasswordAuthentication(
- CredentialFinder(callingActivity, connectionMode)).execute()
- ConnectionMode.None -> execute()
+ AuthMode.OpenKeychain -> withOpenKeychainAuthentication(callingActivity).execute()
+ AuthMode.Password -> withPasswordAuthentication(
+ CredentialFinder(callingActivity, authMode)).execute()
+ AuthMode.None -> execute()
}
}
@@ -162,17 +161,10 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
@CallSuper
open fun onError(err: Exception) {
// Clear various auth related fields on failure
- when (SshSessionFactory.getInstance()) {
- is SshApiSessionFactory -> {
- PreferenceManager.getDefaultSharedPreferences(callingActivity.applicationContext)
- .edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) }
- }
- is SshjSessionFactory -> {
- callingActivity.getEncryptedPrefs("git_operation").edit {
- remove(PreferenceKeys.HTTPS_PASSWORD)
- }
- }
+ callingActivity.getEncryptedPrefs("git_operation").edit {
+ remove(PreferenceKeys.HTTPS_PASSWORD)
}
+ callingActivity.sharedPrefs.edit { remove(PreferenceKeys.SSH_OPENKEYSTORE_KEYID) }
d(err)
MaterialAlertDialogBuilder(callingActivity)
.setTitle(callingActivity.resources.getString(R.string.jgit_error_dialog_title))
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainKeyProvider.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainKeyProvider.kt
new file mode 100644
index 00000000..cecf7505
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainKeyProvider.kt
@@ -0,0 +1,212 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.git.sshj
+
+import android.app.PendingIntent
+import android.content.Intent
+import androidx.activity.result.IntentSenderRequest
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.core.content.edit
+import androidx.fragment.app.FragmentActivity
+import com.github.ajalt.timberkt.d
+import com.zeapo.pwdstore.utils.OPENPGP_PROVIDER
+import com.zeapo.pwdstore.utils.PreferenceKeys
+import com.zeapo.pwdstore.utils.sharedPrefs
+import java.io.Closeable
+import java.security.PublicKey
+import kotlin.coroutines.Continuation
+import kotlin.coroutines.resume
+import kotlin.coroutines.resumeWithException
+import kotlin.coroutines.suspendCoroutine
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import net.schmizz.sshj.common.Base64
+import net.schmizz.sshj.common.Buffer
+import net.schmizz.sshj.common.DisconnectReason
+import net.schmizz.sshj.common.KeyType
+import net.schmizz.sshj.userauth.UserAuthException
+import net.schmizz.sshj.userauth.keyprovider.KeyProvider
+import org.openintents.ssh.authentication.ISshAuthenticationService
+import org.openintents.ssh.authentication.SshAuthenticationApi
+import org.openintents.ssh.authentication.SshAuthenticationApiError
+import org.openintents.ssh.authentication.SshAuthenticationConnection
+import org.openintents.ssh.authentication.request.KeySelectionRequest
+import org.openintents.ssh.authentication.request.Request
+import org.openintents.ssh.authentication.request.SigningRequest
+import org.openintents.ssh.authentication.request.SshPublicKeyRequest
+import org.openintents.ssh.authentication.response.KeySelectionResponse
+import org.openintents.ssh.authentication.response.Response
+import org.openintents.ssh.authentication.response.SigningResponse
+import org.openintents.ssh.authentication.response.SshPublicKeyResponse
+
+class OpenKeychainKeyProvider private constructor(private val activity: FragmentActivity) : KeyProvider, Closeable {
+
+ companion object {
+
+ suspend fun prepareAndUse(activity: FragmentActivity, block: (provider: OpenKeychainKeyProvider) -> Unit) {
+ withContext(Dispatchers.Main){
+ OpenKeychainKeyProvider(activity)
+ }.prepareAndUse(block)
+ }
+ }
+
+ private sealed class ApiResponse {
+ data class Success(val response: Response) : ApiResponse()
+ data class GeneralError(val exception: Exception) : ApiResponse()
+ data class NoSuchKey(val exception: Exception) : ApiResponse()
+ }
+
+ private val context = activity.applicationContext
+ private val sshServiceConnection = SshAuthenticationConnection(context, OPENPGP_PROVIDER)
+ private val preferences = context.sharedPrefs
+ private val continueAfterUserInteraction =
+ activity.registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->
+ currentCont?.let { cont ->
+ currentCont = null
+ val data = result.data
+ if (data != null)
+ cont.resume(data)
+ else
+ cont.resumeWithException(UserAuthException(DisconnectReason.AUTH_CANCELLED_BY_USER))
+ }
+ }
+
+ private lateinit var sshServiceApi: SshAuthenticationApi
+
+ private var currentCont: Continuation<Intent>? = null
+ private var keyId
+ get() = preferences.getString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, null)
+ set(value) {
+ preferences.edit {
+ putString(PreferenceKeys.SSH_OPENKEYSTORE_KEYID, value)
+ }
+ }
+ private var publicKey: PublicKey? = null
+ private var privateKey: OpenKeychainPrivateKey? = null
+
+ private suspend fun prepareAndUse(block: (provider: OpenKeychainKeyProvider) -> Unit) {
+ prepare()
+ use(block)
+ }
+
+ private suspend fun prepare() {
+ sshServiceApi = suspendCoroutine { cont ->
+ sshServiceConnection.connect(object : SshAuthenticationConnection.OnBound {
+ override fun onBound(sshAgent: ISshAuthenticationService) {
+ d { "Bound to SshAuthenticationApi: $OPENPGP_PROVIDER" }
+ cont.resume(SshAuthenticationApi(context, sshAgent))
+ }
+
+ override fun onError() {
+ throw UserAuthException(DisconnectReason.UNKNOWN, "OpenKeychain service unavailable")
+ }
+ })
+ }
+
+ if (keyId == null) {
+ selectKey()
+ }
+ check(keyId != null)
+ fetchPublicKey()
+ makePrivateKey()
+ }
+
+ private suspend fun fetchPublicKey(isRetry: Boolean = false) {
+ when (val sshPublicKeyResponse = executeApiRequest(SshPublicKeyRequest(keyId))) {
+ is ApiResponse.Success -> {
+ val response = sshPublicKeyResponse.response as SshPublicKeyResponse
+ val sshPublicKey = response.sshPublicKey!!
+ val sshKeyParts = sshPublicKey.split("""\s+""".toRegex())
+ check(sshKeyParts.size >= 2) { "OpenKeychain API returned invalid SSH key" }
+ @Suppress("BlockingMethodInNonBlockingContext")
+ publicKey = Buffer.PlainBuffer(Base64.decode(sshKeyParts[1])).readPublicKey()
+ }
+ is ApiResponse.NoSuchKey -> if (isRetry) {
+ throw sshPublicKeyResponse.exception
+ } else {
+ // Allow the user to reselect an authentication key and retry
+ selectKey()
+ fetchPublicKey(true)
+ }
+ is ApiResponse.GeneralError -> throw sshPublicKeyResponse.exception
+ }
+ }
+
+ private suspend fun selectKey() {
+ when (val keySelectionResponse = executeApiRequest(KeySelectionRequest())) {
+ is ApiResponse.Success -> keyId = (keySelectionResponse.response as KeySelectionResponse).keyId
+ is ApiResponse.GeneralError -> throw keySelectionResponse.exception
+ is ApiResponse.NoSuchKey -> throw keySelectionResponse.exception
+ }
+ }
+
+ private suspend fun executeApiRequest(request: Request, resultOfUserInteraction: Intent? = null): ApiResponse {
+ d { "executeRequest($request) called" }
+ val result = withContext(Dispatchers.Main) {
+ // If the request required user interaction, the data returned from the PendingIntent
+ // is used as the real request.
+ sshServiceApi.executeApi(resultOfUserInteraction ?: request.toIntent())!!
+ }
+ return parseResult(request, result).also {
+ d { "executeRequest($request): $it" }
+ }
+ }
+
+ private suspend fun parseResult(request: Request, result: Intent): ApiResponse {
+ return when (result.getIntExtra(SshAuthenticationApi.EXTRA_RESULT_CODE, SshAuthenticationApi.RESULT_CODE_ERROR)) {
+ SshAuthenticationApi.RESULT_CODE_SUCCESS -> {
+ ApiResponse.Success(when (request) {
+ is KeySelectionRequest -> KeySelectionResponse(result)
+ is SshPublicKeyRequest -> SshPublicKeyResponse(result)
+ is SigningRequest -> SigningResponse(result)
+ else -> throw IllegalArgumentException("Unsupported OpenKeychain request type")
+ })
+ }
+ SshAuthenticationApi.RESULT_CODE_USER_INTERACTION_REQUIRED -> {
+ val pendingIntent: PendingIntent = result.getParcelableExtra(SshAuthenticationApi.EXTRA_PENDING_INTENT)!!
+ val resultOfUserInteraction: Intent = withContext(Dispatchers.Main) {
+ suspendCoroutine { cont ->
+ currentCont = cont
+ continueAfterUserInteraction.launch(IntentSenderRequest.Builder(pendingIntent).build())
+ }
+ }
+ executeApiRequest(request, resultOfUserInteraction)
+ }
+ else -> {
+ val error = result.getParcelableExtra<SshAuthenticationApiError>(SshAuthenticationApi.EXTRA_ERROR)
+ val exception = UserAuthException(DisconnectReason.UNKNOWN, "Request ${request::class.simpleName} failed: ${error?.message}")
+ when (error?.error) {
+ SshAuthenticationApiError.NO_AUTH_KEY, SshAuthenticationApiError.NO_SUCH_KEY -> ApiResponse.NoSuchKey(exception)
+ else -> ApiResponse.GeneralError(exception)
+ }
+ }
+ }
+ }
+
+ private fun makePrivateKey() {
+ check(keyId != null && publicKey != null)
+ privateKey = object : OpenKeychainPrivateKey {
+ override suspend fun sign(challenge: ByteArray, hashAlgorithm: Int) =
+ when (val signingResponse = executeApiRequest(SigningRequest(challenge, keyId, hashAlgorithm))) {
+ is ApiResponse.Success -> (signingResponse.response as SigningResponse).signature
+ is ApiResponse.GeneralError -> throw signingResponse.exception
+ is ApiResponse.NoSuchKey -> throw signingResponse.exception
+ }
+
+ override fun getAlgorithm() = publicKey!!.algorithm
+ }
+ }
+
+ override fun close() {
+ continueAfterUserInteraction.unregister()
+ sshServiceConnection.disconnect()
+ }
+
+ override fun getPrivate() = privateKey
+
+ override fun getPublic() = publicKey
+
+ override fun getType() = KeyType.fromKey(publicKey)
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt
new file mode 100644
index 00000000..97b587fd
--- /dev/null
+++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/OpenKeychainWrappedKeyAlgorithmFactory.kt
@@ -0,0 +1,93 @@
+/*
+ * Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
+ * SPDX-License-Identifier: GPL-3.0-only
+ */
+package com.zeapo.pwdstore.git.sshj
+
+import com.hierynomus.sshj.key.KeyAlgorithm
+import java.io.ByteArrayOutputStream
+import java.security.PrivateKey
+import kotlinx.coroutines.runBlocking
+import net.schmizz.sshj.common.Buffer
+import net.schmizz.sshj.common.Factory
+import net.schmizz.sshj.signature.Signature
+import org.openintents.ssh.authentication.SshAuthenticationApi
+
+interface OpenKeychainPrivateKey : PrivateKey {
+
+ suspend fun sign(challenge: ByteArray, hashAlgorithm: Int): ByteArray
+
+ override fun getFormat() = null
+ override fun getEncoded() = null
+}
+
+class OpenKeychainWrappedKeyAlgorithmFactory(private val factory: Factory.Named<KeyAlgorithm>) : Factory.Named<KeyAlgorithm> by factory {
+
+ override fun create() = OpenKeychainWrappedKeyAlgorithm(factory.create())
+}
+
+class OpenKeychainWrappedKeyAlgorithm(private val keyAlgorithm: KeyAlgorithm) : KeyAlgorithm by keyAlgorithm {
+
+ private val hashAlgorithm = when (keyAlgorithm.keyAlgorithm) {
+ "rsa-sha2-512" -> SshAuthenticationApi.SHA512
+ "rsa-sha2-256" -> SshAuthenticationApi.SHA256
+ "ssh-rsa", "ssh-rsa-cert-v01@openssh.com" -> SshAuthenticationApi.SHA1
+ // Other algorithms don't use this value, but it has to be valid.
+ else -> SshAuthenticationApi.SHA512
+ }
+
+ override fun newSignature() = OpenKeychainWrappedSignature(keyAlgorithm.newSignature(), hashAlgorithm)
+}
+
+class OpenKeychainWrappedSignature(private val wrappedSignature: Signature, private val hashAlgorithm: Int) : Signature by wrappedSignature {
+
+ private val data = ByteArrayOutputStream()
+
+ private var bridgedPrivateKey: OpenKeychainPrivateKey? = null
+
+ override fun initSign(prvkey: PrivateKey?) {
+ if (prvkey is OpenKeychainPrivateKey) {
+ bridgedPrivateKey = prvkey
+ } else {
+ wrappedSignature.initSign(prvkey)
+ }
+ }
+
+ override fun update(H: ByteArray?) {
+ if (bridgedPrivateKey != null) {
+ data.write(H!!)
+ } else {
+ wrappedSignature.update(H)
+ }
+ }
+
+ override fun update(H: ByteArray?, off: Int, len: Int) {
+ if (bridgedPrivateKey != null) {
+ data.write(H!!, off, len)
+ } else {
+ wrappedSignature.update(H, off, len)
+ }
+ }
+
+ override fun sign(): ByteArray? = if (bridgedPrivateKey != null) {
+ runBlocking {
+ bridgedPrivateKey!!.sign(data.toByteArray(), hashAlgorithm)
+ }
+ } else {
+ wrappedSignature.sign()
+ }
+
+ override fun encode(signature: ByteArray?): ByteArray? = if (bridgedPrivateKey != null) {
+ require(signature != null) { "OpenKeychain signature must not be null" }
+ val encodedSignature = Buffer.PlainBuffer(signature)
+ // We need to drop the algorithm name and extract the raw signature since SSHJ adds the name
+ // later.
+ encodedSignature.readString()
+ encodedSignature.readBytes().also {
+ bridgedPrivateKey = null
+ data.reset()
+ }
+ } else {
+ wrappedSignature.encode(signature)
+ }
+}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjConfig.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt
index 6c409329..bf454cd5 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjConfig.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjConfig.kt
@@ -2,7 +2,7 @@
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
-package com.zeapo.pwdstore.git.config
+package com.zeapo.pwdstore.git.sshj
import com.github.ajalt.timberkt.Timber
import com.github.ajalt.timberkt.d
@@ -232,7 +232,9 @@ class SshjConfig : ConfigImpl() {
KeyAlgorithms.ECDSASHANistp384(),
KeyAlgorithms.ECDSASHANistp256(),
KeyAlgorithms.SSHRSA(),
- )
+ ).map {
+ OpenKeychainWrappedKeyAlgorithmFactory(it)
+ }
}
private fun initRandomFactory() {
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjSessionFactory.kt
index 85b5f753..05428e41 100644
--- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/git/sshj/SshjSessionFactory.kt
@@ -2,9 +2,10 @@
* Copyright © 2014-2020 The Android Password Store Authors. All Rights Reserved.
* SPDX-License-Identifier: GPL-3.0-only
*/
-package com.zeapo.pwdstore.git.config
+package com.zeapo.pwdstore.git.sshj
import android.util.Base64
+import androidx.fragment.app.FragmentActivity
import com.github.ajalt.timberkt.d
import com.github.ajalt.timberkt.w
import java.io.File
@@ -37,6 +38,7 @@ import org.eclipse.jgit.util.FS
sealed class SshAuthData {
class Password(val passwordFinder: InteractivePasswordFinder) : SshAuthData()
class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData()
+ class OpenKeychain(val activity: FragmentActivity) : SshAuthData()
}
abstract class InteractivePasswordFinder : PasswordFinder {
@@ -128,6 +130,13 @@ private class SshjSession(uri: URIish, private val username: String, private val
is SshAuthData.PublicKeyFile -> {
ssh.authPublickey(username, ssh.loadKeys(authData.keyFile.absolutePath, authData.passphraseFinder))
}
+ is SshAuthData.OpenKeychain -> {
+ runBlocking {
+ OpenKeychainKeyProvider.prepareAndUse(authData.activity) { provider ->
+ ssh.authPublickey(username, provider)
+ }
+ }
+ }
}
return this
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt b/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt
index dc9d0ebd..2915efb4 100644
--- a/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/ui/adapters/PasswordItemRecyclerAdapter.kt
@@ -50,7 +50,7 @@ open class PasswordItemRecyclerAdapter :
fun bind(item: PasswordItem) {
val settings = itemView.context.sharedPrefs
- val showHidden = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, false)
+ val showHidden = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
val parentPath = item.fullPathToParent.replace("(^/)|(/$)".toRegex(), "")
val source = if (parentPath.isNotEmpty()) {
"$parentPath\n$item"
@@ -62,8 +62,8 @@ open class PasswordItemRecyclerAdapter :
name.text = spannable
if (item.type == PasswordItem.TYPE_CATEGORY) {
folderIndicator.visibility = View.VISIBLE
- val children = item.file.listFiles { pathname ->
- !(!showHidden && (pathname.isDirectory && pathname.isHidden))
+ val children = with(item.file) {
+ if (showHidden) listFiles() else listFiles { pathname -> pathname.isDirectory && !pathname.isHidden }
} ?: emptyArray<File>()
val count = children.size
childCount.visibility = if (count > 0) View.VISIBLE else View.GONE
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/BiometricAuthenticator.kt b/app/src/main/java/com/zeapo/pwdstore/utils/BiometricAuthenticator.kt
index d29e7ac4..0792c1fc 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/BiometricAuthenticator.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/BiometricAuthenticator.kt
@@ -9,6 +9,7 @@ import android.os.Handler
import androidx.annotation.StringRes
import androidx.biometric.BiometricConstants
import androidx.biometric.BiometricManager
+import androidx.biometric.BiometricManager.Authenticators
import androidx.biometric.BiometricPrompt
import androidx.core.content.getSystemService
import androidx.fragment.app.FragmentActivity
@@ -60,14 +61,15 @@ object BiometricAuthenticator {
callback(Result.Success(result.cryptoObject))
}
}
- val biometricPrompt = BiometricPrompt(activity, { handler.post(it) }, authCallback)
- val promptInfo = BiometricPrompt.PromptInfo.Builder()
- .setTitle(activity.getString(dialogTitleRes))
- .setDeviceCredentialAllowed(true)
- .build()
- if (BiometricManager.from(activity).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS ||
- activity.getSystemService<KeyguardManager>()?.isDeviceSecure == true) {
- biometricPrompt.authenticate(promptInfo)
+ val validAuthenticators = Authenticators.DEVICE_CREDENTIAL or Authenticators.BIOMETRIC_WEAK
+ val canAuth = BiometricManager.from(activity).canAuthenticate(validAuthenticators) == BiometricManager.BIOMETRIC_SUCCESS
+ val deviceHasKeyguard = activity.getSystemService<KeyguardManager>()?.isDeviceSecure == true
+ if (canAuth || deviceHasKeyguard) {
+ val promptInfo = BiometricPrompt.PromptInfo.Builder()
+ .setTitle(activity.getString(dialogTitleRes))
+ .setAllowedAuthenticators(validAuthenticators)
+ .build()
+ BiometricPrompt(activity, { handler.post(it) }, authCallback).authenticate(promptInfo)
} else {
callback(Result.HardwareUnavailableOrDisabled)
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
index 96bf1b7e..561b8d99 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/Extensions.kt
@@ -9,6 +9,7 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.os.Build
+import android.util.Base64
import android.util.TypedValue
import android.view.View
import android.view.autofill.AutofillManager
@@ -42,6 +43,10 @@ fun String.splitLines(): Array<String> {
return split("\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
}
+fun String.base64(): String {
+ return Base64.encodeToString(encodeToByteArray(), Base64.NO_WRAP)
+}
+
val Context.clipboard get() = getSystemService<ClipboardManager>()
fun FragmentActivity.snackbar(
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt
index 016ee1aa..ef31324b 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordItem.kt
@@ -33,7 +33,7 @@ data class PasswordItem(
}
override fun toString(): String {
- return name.replace(".gpg", "")
+ return name.replace("\\.gpg$".toRegex(), "")
}
override fun hashCode(): Int {
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt
index 8a49f0e3..73660fca 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt
@@ -4,6 +4,7 @@
*/
package com.zeapo.pwdstore.utils
+import android.content.Context
import android.content.SharedPreferences
import androidx.core.content.edit
import com.zeapo.pwdstore.Application
@@ -31,6 +32,18 @@ open class PasswordRepository protected constructor() {
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)
});
@@ -213,12 +226,10 @@ open class PasswordRepository protected constructor() {
// We need to recover the passwords then parse the files
val passList = getFilesList(path).also { it.sortBy { f -> f.name } }
val passwordList = ArrayList<PasswordItem>()
- val showHiddenDirs = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_FOLDERS, false)
+ val showHidden = settings.getBoolean(PreferenceKeys.SHOW_HIDDEN_CONTENTS, false)
if (passList.size == 0) return passwordList
- if (showHiddenDirs) {
- passList.filter { !(it.isFile && it.isHidden) }.toCollection(passList.apply { clear() })
- } else {
+ if (!showHidden) {
passList.filter { !it.isHidden }.toCollection(passList.apply { clear() })
}
passList.forEach { file ->
@@ -231,47 +242,5 @@ open class PasswordRepository protected constructor() {
passwordList.sortWith(sortOrder.comparator)
return passwordList
}
-
- /**
- * Sets the git user name
- *
- * @param username username
- */
- @JvmStatic
- fun setGitAuthorName(username: String) {
- setStringConfig("user", null, "name", username)
- }
-
- /**
- * Sets the git user email
- *
- * @param email email
- */
- @JvmStatic
- fun setGitAuthorEmail(email: String) {
- setStringConfig("user", null, "email", email)
- }
-
- /**
- * Sets a git config value
- *
- * @param section config section name
- * @param subsection config subsection name
- * @param name config name
- * @param value the value to be set
- */
- @JvmStatic
- @Suppress("SameParameterValue")
- private fun setStringConfig(section: String, subsection: String?, name: String, value: String) {
- if (isInitialized) {
- val config = repository!!.config
- config.setString(section, subsection, name, value)
- try {
- config.save()
- } catch (e: Exception) {
- e.printStackTrace()
- }
- }
- }
}
}
diff --git a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
index 05fb9326..bcda4505 100644
--- a/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/utils/PreferenceKeys.kt
@@ -28,15 +28,21 @@ object PreferenceKeys {
const val GIT_EXTERNAL = "git_external"
const val GIT_EXTERNAL_REPO = "git_external_repo"
const val GIT_REMOTE_AUTH = "git_remote_auth"
+
@Deprecated("Use GIT_REMOTE_URL instead")
const val GIT_REMOTE_LOCATION = "git_remote_location"
+
@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"
@@ -54,7 +60,13 @@ object PreferenceKeys {
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"
diff --git a/app/src/main/res/layout/activity_git_clone.xml b/app/src/main/res/layout/activity_git_clone.xml
index 7c59572c..b7ec6fc1 100644
--- a/app/src/main/res/layout/activity_git_clone.xml
+++ b/app/src/main/res/layout/activity_git_clone.xml
@@ -29,42 +29,6 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
- <androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/label_server_protocol"
- style="@style/TextAppearance.MaterialComponents.Headline6"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_margin="8dp"
- android:text="@string/server_protocol"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/server_label" />
-
- <com.google.android.material.button.MaterialButtonToggleGroup
- android:id="@+id/clone_protocol_group"
- style="@style/TextAppearance.MaterialComponents.Headline1"
- android:layout_width="0dp"
- android:layout_height="wrap_content"
- android:layout_margin="8dp"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/label_server_protocol"
- app:selectionRequired="true"
- app:singleSelection="true">
-
- <com.google.android.material.button.MaterialButton
- android:id="@+id/clone_protocol_ssh"
- style="?attr/materialButtonOutlinedStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/clone_protocol_ssh" />
-
- <com.google.android.material.button.MaterialButton
- android:id="@+id/clone_protocol_https"
- style="?attr/materialButtonOutlinedStyle"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:text="@string/clone_protocol_https" />
- </com.google.android.material.button.MaterialButtonToggleGroup>
-
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/label_server_url"
android:layout_width="0dp"
@@ -73,15 +37,15 @@
android:hint="@string/server_url"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/clone_protocol_group">
+ app:layout_constraintTop_toBottomOf="@id/server_label">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_url"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionNext"
- android:nextFocusForward="@id/server_branch"
- android:inputType="textWebEmailAddress" />
+ android:inputType="textWebEmailAddress"
+ android:nextFocusForward="@id/server_branch" />
</com.google.android.material.textfield.TextInputLayout>
@@ -91,21 +55,21 @@
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="@string/server_branch"
- app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/label_server_url">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/server_branch"
- android:imeOptions="actionDone"
android:layout_width="match_parent"
- android:inputType="textNoSuggestions"
- android:layout_height="wrap_content" />
+ android:layout_height="wrap_content"
+ android:imeOptions="actionDone"
+ android:inputType="textNoSuggestions" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.appcompat.widget.AppCompatTextView
- android:id="@+id/label_connection_mode"
+ android:id="@+id/label_auth_mode"
style="@style/TextAppearance.MaterialComponents.Headline6"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -117,31 +81,31 @@
app:layout_constraintTop_toBottomOf="@id/label_server_branch" />
<com.google.android.material.button.MaterialButtonToggleGroup
- android:id="@+id/connection_mode_group"
+ android:id="@+id/auth_mode_group"
style="@style/TextAppearance.MaterialComponents.Headline1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/label_connection_mode"
+ app:layout_constraintTop_toBottomOf="@id/label_auth_mode"
app:singleSelection="true">
<com.google.android.material.button.MaterialButton
- android:id="@+id/connection_mode_ssh_key"
+ android:id="@+id/auth_mode_ssh_key"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connection_mode_ssh_key" />
<com.google.android.material.button.MaterialButton
- android:id="@+id/connection_mode_password"
+ android:id="@+id/auth_mode_password"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/connection_mode_basic_authentication" />
<com.google.android.material.button.MaterialButton
- android:id="@+id/connection_mode_open_keychain"
+ android:id="@+id/auth_mode_open_keychain"
style="?attr/materialButtonOutlinedStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@@ -155,6 +119,6 @@
android:layout_marginTop="8dp"
android:text="@string/crypto_save"
app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@id/connection_mode_group" />
+ app:layout_constraintTop_toBottomOf="@id/auth_mode_group" />
</androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>
diff --git a/app/src/main/res/layout/password_creation_activity.xml b/app/src/main/res/layout/password_creation_activity.xml
index 9440d128..8e2f42b1 100644
--- a/app/src/main/res/layout/password_creation_activity.xml
+++ b/app/src/main/res/layout/password_creation_activity.xml
@@ -3,118 +3,125 @@
~ SPDX-License-Identifier: GPL-3.0-only
-->
-<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
- android:orientation="vertical"
- android:padding="@dimen/activity_horizontal_margin"
+ android:fillViewport="false"
tools:context="com.zeapo.pwdstore.crypto.PasswordCreationActivity">
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/directory_input_layout"
+ <androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:layout_margin="8dp"
- android:enabled="false"
- android:hint="@string/directory_hint"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toTopOf="parent">
+ android:orientation="vertical"
+ android:padding="@dimen/activity_horizontal_margin">
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/directory"
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/directory_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:imeOptions="actionNext"
- android:inputType="textNoSuggestions"
- android:nextFocusForward="@id/password"
- tools:text="CATEGORY HERE" />
- </com.google.android.material.textfield.TextInputLayout>
+ android:layout_gravity="center_vertical"
+ android:layout_margin="8dp"
+ android:enabled="false"
+ android:hint="@string/directory_hint"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="parent">
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/name_input_layout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
- android:layout_margin="8dp"
- android:hint="@string/crypto_name_hint"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/directory_input_layout">
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/directory"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions"
+ android:nextFocusForward="@id/password"
+ tools:text="CATEGORY HERE" />
+ </com.google.android.material.textfield.TextInputLayout>
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/filename"
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/name_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:imeOptions="actionNext"
- android:inputType="textNoSuggestions"
- android:nextFocusForward="@id/password" />
- </com.google.android.material.textfield.TextInputLayout>
+ android:layout_gravity="center_vertical"
+ android:layout_margin="8dp"
+ android:hint="@string/crypto_name_hint"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/directory_input_layout">
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/password_input_layout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_margin="8dp"
- android:hint="@string/crypto_pass_label"
- app:endIconMode="password_toggle"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/name_input_layout">
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/filename"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionNext"
+ android:inputType="textNoSuggestions"
+ android:nextFocusForward="@id/password" />
+ </com.google.android.material.textfield.TextInputLayout>
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/password"
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/password_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:imeOptions="actionDone"
- android:inputType="textVisiblePassword" />
- </com.google.android.material.textfield.TextInputLayout>
+ android:layout_margin="8dp"
+ android:hint="@string/crypto_pass_label"
+ app:endIconMode="password_toggle"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/name_input_layout">
- <com.google.android.material.button.MaterialButton
- android:id="@+id/generate_password"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_margin="8dp"
- android:text="@string/pwd_generate_button"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@id/password_input_layout" />
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/password"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:imeOptions="actionDone"
+ android:inputType="textVisiblePassword" />
+ </com.google.android.material.textfield.TextInputLayout>
- <com.google.android.material.textfield.TextInputLayout
- android:id="@+id/extra_input_layout"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_margin="8dp"
- android:hint="@string/crypto_extra_label"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/generate_password">
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/generate_password"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/pwd_generate_button"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/password_input_layout" />
- <com.google.android.material.textfield.TextInputEditText
- android:id="@+id/extra_content"
+ <com.google.android.material.textfield.TextInputLayout
+ android:id="@+id/extra_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:inputType="textMultiLine|textVisiblePassword" />
+ android:layout_margin="8dp"
+ android:hint="@string/crypto_extra_label"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/generate_password">
- </com.google.android.material.textfield.TextInputLayout>
+ <com.google.android.material.textfield.TextInputEditText
+ android:id="@+id/extra_content"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textMultiLine|textVisiblePassword" />
- <com.google.android.material.button.MaterialButton
- android:id="@+id/otp_import_button"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_margin="8dp"
- android:text="@string/add_otp"
- app:icon="@drawable/ic_qr_code_scanner"
- app:iconTint="?attr/colorOnSecondary"
- app:layout_constraintEnd_toEndOf="parent"
- app:layout_constraintTop_toBottomOf="@id/extra_input_layout" />
+ </com.google.android.material.textfield.TextInputLayout>
- <com.google.android.material.switchmaterial.SwitchMaterial
- android:id="@+id/encrypt_username"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:layout_margin="8dp"
- android:text="@string/crypto_encrypt_username_label"
- android:visibility="gone"
- app:layout_constraintStart_toStartOf="parent"
- app:layout_constraintTop_toBottomOf="@id/otp_import_button"
- tools:visibility="visible" />
-</androidx.constraintlayout.widget.ConstraintLayout>
+ <com.google.android.material.button.MaterialButton
+ android:id="@+id/otp_import_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/add_otp"
+ app:icon="@drawable/ic_qr_code_scanner"
+ app:iconTint="?attr/colorOnSecondary"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/extra_input_layout" />
+
+ <com.google.android.material.switchmaterial.SwitchMaterial
+ android:id="@+id/encrypt_username"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_margin="8dp"
+ android:text="@string/crypto_encrypt_username_label"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toBottomOf="@id/otp_import_button"
+ tools:visibility="visible" />
+ </androidx.constraintlayout.widget.ConstraintLayout>
+</ScrollView>
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 79edf2d9..a7f3e046 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -269,8 +269,6 @@
<string name="access_sdcard_text">O local do armazenamento está em seu cartão SD ou armazenamento interno, mas o aplicativo não tem permissão para acessá-lo.</string>
<string name="your_public_key">Sua chave pública</string>
<string name="error_generate_ssh_key">Erro ao tentar gerar a chave SSH</string>
- <string name="pref_show_hidden_title">Mostrar pastas ocultas</string>
- <string name="pref_show_hidden_summary">Incluir diretórios ocultos na lista de senhas</string>
<string name="title_create_folder">Criar pasta</string>
<string name="title_rename_folder">Renomear pasta</string>
<string name="message_category_error_empty_field">O nome da categoria não pode ser vazio</string>
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index 596e1c14..3fab8cf9 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -251,8 +251,6 @@
<string name="ssh_openkeystore_clear_keyid">Очистить сохраненный SSH Key идентификатор OpenKystortore</string>
<string name="your_public_key">Ваш публичный ключ</string>
<string name="error_generate_ssh_key">Возникла ошибка при попытке генерации ssh ключа</string>
- <string name="pref_show_hidden_title">Показать скрытые папки</string>
- <string name="pref_show_hidden_summary">Включить скрытые директории в список паролей</string>
<string name="title_create_folder">Создать папку</string>
<string name="button_create">Создать</string>
<string name="pref_search_on_start">Открыть поиск на старте</string>
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index 6b5edf6c..2b320f31 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -8,11 +8,13 @@
<item>@string/pref_folder_first_sort_order</item>
<item>@string/pref_file_first_sort_order</item>
<item>@string/pref_type_independent_sort_order</item>
+ <item>@string/pref_recently_used_sort_order</item>
</string-array>
<string-array name="sort_order_values">
<item>FOLDER_FIRST</item>
<item>FILE_FIRST</item>
<item>INDEPENDENT</item>
+ <item>RECENTLY_USED</item>
</string-array>
<string-array name="capitalization_type_values">
<item>lowercase</item>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a7a9c141..a6e33047 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -140,6 +140,7 @@
<string name="pref_folder_first_sort_order">Folders first</string>
<string name="pref_file_first_sort_order">Files first</string>
<string name="pref_type_independent_sort_order">Type independent</string>
+ <string name="pref_recently_used_sort_order">Recently used</string>
<string name="pref_autofill_title">Autofill</string>
<string name="pref_autofill_enable_title">Enable Autofill</string>
<string name="pref_autofill_enable_msg">Tap OK to go to Accessibility settings. There, tap Password Store under Services then tap the switch in the top right to turn it on or off.</string>
@@ -301,8 +302,8 @@
<string name="access_sdcard_text">The store location is in your SD Card or Internal storage, but the app does not have permission to access it.</string>
<string name="your_public_key">Your public key</string>
<string name="error_generate_ssh_key">Error while trying to generate the ssh-key</string>
- <string name="pref_show_hidden_title">Show hidden folders</string>
- <string name="pref_show_hidden_summary">Include hidden directories in the password list</string>
+ <string name="pref_show_hidden_title">Show all files and folders</string>
+ <string name="pref_show_hidden_summary">Include non-password files and directories in the password list</string>
<string name="title_create_folder">Create folder</string>
<string name="title_rename_folder">Rename folder</string>
<string name="message_category_error_empty_field">Category name can\'t be empty</string>
@@ -327,6 +328,9 @@
<string name="connection_mode_openkeychain" translatable="false">OpenKeychain</string>
<string name="git_server_config_save_success">Successfully saved configuration</string>
<string name="git_server_config_save_error">The provided repository URL is not valid</string>
+ <string name="git_server_config_save_missing_username_https">Please specify the HTTPS username in the form https://username@example.com/…</string>
+ <string name="git_server_config_save_missing_username_ssh">Please specify the SSH username in the form username@example.com:…</string>
+ <string name="git_server_config_save_auth_mode_mismatch">Valid authentication modes for %1$s: %2$s</string>
<string name="git_config_error_hostname_empty">empty hostname</string>
<string name="git_config_error_generic">please verify your settings and try again</string>
<string name="git_config_error_nonnumeric_port">port must be numeric</string>
diff --git a/app/src/main/res/xml/oreo_autofill_service.xml b/app/src/main/res/xml/oreo_autofill_service.xml
index 8b76c803..7e761e25 100644
--- a/app/src/main/res/xml/oreo_autofill_service.xml
+++ b/app/src/main/res/xml/oreo_autofill_service.xml
@@ -14,6 +14,7 @@
<compatibility-package android:name="com.microsoft.emmx" />
<compatibility-package android:name="com.opera.mini.native" />
<compatibility-package android:name="com.opera.mini.native.beta" />
+ <compatibility-package android:name="org.bromite.bromite" />
<compatibility-package
android:name="org.mozilla.fennec_fdroid"
android:maxLongVersionCode="679999" />
@@ -23,4 +24,5 @@
<compatibility-package
android:name="org.mozilla.firefox_beta"
android:maxLongVersionCode="679999" />
+ <compatibility-package android:name="org.ungoogled.chromium.stable" />
</autofill-service>
diff --git a/app/src/main/res/xml/preference.xml b/app/src/main/res/xml/preference.xml
index 50300b5b..1d82df18 100644
--- a/app/src/main/res/xml/preference.xml
+++ b/app/src/main/res/xml/preference.xml
@@ -127,7 +127,7 @@
app:title="@string/pref_search_on_start" />
<CheckBoxPreference
app:defaultValue="false"
- app:key="show_hidden_folders"
+ app:key="show_hidden_contents"
app:persistent="true"
app:summary="@string/pref_show_hidden_summary"
app:title="@string/pref_show_hidden_title" />