summaryrefslogtreecommitdiff
path: root/app/src
diff options
context:
space:
mode:
Diffstat (limited to 'app/src')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/Application.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt12
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/PasswordStore.kt13
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt1
-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/config/SshApiSessionFactory.java383
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/CredentialFinder.kt2
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/operation/GitOperation.kt30
-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/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/PasswordRepository.kt55
-rw-r--r--app/src/main/res/layout/password_creation_activity.xml183
-rw-r--r--app/src/main/res/values/arrays.xml2
-rw-r--r--app/src/main/res/values/strings.xml1
-rw-r--r--app/src/main/res/xml/oreo_autofill_service.xml2
24 files changed, 515 insertions, 625 deletions
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/PasswordFragment.kt b/app/src/main/java/com/zeapo/pwdstore/PasswordFragment.kt
index 6657afaf..9e50ad2b 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
@@ -32,6 +33,9 @@ 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
@@ -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..d70417bc 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
@@ -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
@@ -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..0509a3dd 100644
--- a/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt
+++ b/app/src/main/java/com/zeapo/pwdstore/SearchableRepositoryViewModel.kt
@@ -96,6 +96,7 @@ private fun PasswordItem.Companion.makeComparator(
// declare them all equal at this stage.
PasswordRepository.PasswordSortOrder.INDEPENDENT -> Comparator<PasswordItem> { _, _ -> 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)
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..21795404 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.connectionMode)
} 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/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..6b6b8ea8 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
@@ -11,7 +11,7 @@ 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.sshj.InteractivePasswordFinder
import com.zeapo.pwdstore.utils.PreferenceKeys
import com.zeapo.pwdstore.utils.getEncryptedPrefs
import com.zeapo.pwdstore.utils.requestInputFocusOnView
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 62d6879c..3efd3bd8 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,7 +8,6 @@ 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
@@ -16,13 +15,13 @@ import com.zeapo.pwdstore.UserPreference
import com.zeapo.pwdstore.git.ErrorMessages
import com.zeapo.pwdstore.git.config.ConnectionMode
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
@@ -85,8 +84,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
}
@@ -117,7 +117,6 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
suspend fun executeAfterAuthentication(
connectionMode: ConnectionMode,
- identity: SshApiSessionFactory.ApiIdentity?
) {
when (connectionMode) {
ConnectionMode.SshKey -> if (!sshKeyFile.exists()) {
@@ -138,7 +137,7 @@ abstract class GitOperation(gitDir: File, internal val callingActivity: Fragment
withPublicKeyAuthentication(
CredentialFinder(callingActivity, connectionMode)).execute()
}
- ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(identity).execute()
+ ConnectionMode.OpenKeychain -> withOpenKeychainAuthentication(callingActivity).execute()
ConnectionMode.Password -> withPasswordAuthentication(
CredentialFinder(callingActivity, connectionMode)).execute()
ConnectionMode.None -> execute()
@@ -151,17 +150,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.SSH_KEY_LOCAL_PASSPHRASE)
}
+ 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/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/PasswordRepository.kt b/app/src/main/java/com/zeapo/pwdstore/utils/PasswordRepository.kt
index 8a49f0e3..f0b891fc 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)
});
@@ -231,47 +244,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/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/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..1b32e29c 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>
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>