summaryrefslogtreecommitdiff
path: root/app/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java')
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/Application.kt2
-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.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
10 files changed, 335 insertions, 483 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/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..e98f66ad 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,7 @@ 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.sshj.SshjSessionFactory
import com.zeapo.pwdstore.git.operation.GitOperation
import com.zeapo.pwdstore.utils.Result
import com.zeapo.pwdstore.utils.snackbar
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 881b8807..ae4674fe 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
@@ -84,8 +83,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
}
@@ -116,7 +116,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()) {
@@ -137,7 +136,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()
@@ -150,17 +149,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
}