aboutsummaryrefslogtreecommitdiffstats
path: root/app/src/main/java/com/zeapo/pwdstore/git/config
diff options
context:
space:
mode:
authorFabian Henneke <FabianHenneke@users.noreply.github.com>2020-08-18 22:02:34 +0200
committerGitHub <noreply@github.com>2020-08-18 22:02:34 +0200
commit152d86ec3a951bd6cb797128094d933d2c3a5697 (patch)
tree6f8dcc7157f130e3eb2c018cc2819963e0d6993a /app/src/main/java/com/zeapo/pwdstore/git/config
parentbd0d97d242f7e97ae495dd9408c25eed3c208097 (diff)
downloadHardenedPass-152d86ec3a951bd6cb797128094d933d2c3a5697.tar.gz
HardenedPass-152d86ec3a951bd6cb797128094d933d2c3a5697.zip
Add SSHJ backend for OpenKeychain authentication (#995)
* Update sshj to 0.30.0 and improve algorithm order Updates sshj to 0.30.0, which brings support for rsa-sha2-* key types and bugfixes related to RSA certificates and Android Keystore backed keys. Along the way, this improves the algorithm preferences to be consistent with the Mozilla Intermediate SSH configuration (as far as possible, given that most certificate types and some encryption algorithms are not yet supported). We also add "ext-info-c" to the kex algorithm proposal to work around certain kinds of "user agent sniffing" that limits the support of rsa-sha2-* key types. * Add SSHJ backend for OpenKeychain authentication * Address review comments Co-authored-by: Harsh Shandilya <me@msfjarvis.dev>
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git/config')
-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/config/SshjConfig.kt275
-rw-r--r--app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt180
3 files changed, 0 insertions, 838 deletions
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/config/SshjConfig.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/SshjConfig.kt
deleted file mode 100644
index 6c409329..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjConfig.kt
+++ /dev/null
@@ -1,275 +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 com.github.ajalt.timberkt.Timber
-import com.github.ajalt.timberkt.d
-import com.hierynomus.sshj.key.KeyAlgorithms
-import com.hierynomus.sshj.transport.cipher.BlockCiphers
-import com.hierynomus.sshj.transport.kex.ExtInfoClientFactory
-import com.hierynomus.sshj.transport.mac.Macs
-import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile
-import java.security.Security
-import net.schmizz.keepalive.KeepAliveProvider
-import net.schmizz.sshj.ConfigImpl
-import net.schmizz.sshj.common.LoggerFactory
-import net.schmizz.sshj.transport.compression.NoneCompression
-import net.schmizz.sshj.transport.kex.Curve25519SHA256
-import net.schmizz.sshj.transport.kex.Curve25519SHA256.FactoryLibSsh
-import net.schmizz.sshj.transport.kex.DHGexSHA256
-import net.schmizz.sshj.transport.kex.ECDHNistP
-import net.schmizz.sshj.transport.random.JCERandom
-import net.schmizz.sshj.transport.random.SingletonRandomFactory
-import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile
-import net.schmizz.sshj.userauth.keyprovider.PKCS5KeyFile
-import net.schmizz.sshj.userauth.keyprovider.PKCS8KeyFile
-import net.schmizz.sshj.userauth.keyprovider.PuTTYKeyFile
-import org.bouncycastle.jce.provider.BouncyCastleProvider
-import org.slf4j.Logger
-import org.slf4j.Marker
-
-
-fun setUpBouncyCastleForSshj() {
- // Replace the Android BC provider with the Java BouncyCastle provider since the former does
- // not include all the required algorithms.
- // Note: This may affect crypto operations in other parts of the application.
- val bcIndex = Security.getProviders().indexOfFirst {
- it.name == BouncyCastleProvider.PROVIDER_NAME
- }
- if (bcIndex == -1) {
- // No Android BC found, install Java BC at lowest priority.
- Security.addProvider(BouncyCastleProvider())
- } else {
- // Replace Android BC with Java BC, inserted at the same position.
- Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME)
- // May be needed on Android Pie+ as per https://stackoverflow.com/a/57897224/297261
- try {
- Class.forName("sun.security.jca.Providers")
- } catch (e: ClassNotFoundException) {
- }
- Security.insertProviderAt(BouncyCastleProvider(), bcIndex + 1)
- }
- d { "JCE providers: ${Security.getProviders().joinToString { "${it.name} (${it.version})" }}" }
-}
-
-private abstract class AbstractLogger(private val name: String) : Logger {
-
- abstract fun t(message: String, t: Throwable? = null, vararg args: Any?)
- abstract fun d(message: String, t: Throwable? = null, vararg args: Any?)
- abstract fun i(message: String, t: Throwable? = null, vararg args: Any?)
- abstract fun w(message: String, t: Throwable? = null, vararg args: Any?)
- abstract fun e(message: String, t: Throwable? = null, vararg args: Any?)
-
- override fun getName() = name
-
- override fun isTraceEnabled(marker: Marker?): Boolean = isTraceEnabled
- override fun isDebugEnabled(marker: Marker?): Boolean = isDebugEnabled
- override fun isInfoEnabled(marker: Marker?): Boolean = isInfoEnabled
- override fun isWarnEnabled(marker: Marker?): Boolean = isWarnEnabled
- override fun isErrorEnabled(marker: Marker?): Boolean = isErrorEnabled
-
- override fun trace(msg: String) = t(msg)
- override fun trace(format: String, arg: Any?) = t(format, null, arg)
- override fun trace(format: String, arg1: Any?, arg2: Any?) = t(format, null, arg1, arg2)
- override fun trace(format: String, vararg arguments: Any?) = t(format, null, *arguments)
- override fun trace(msg: String, t: Throwable?) = t(msg, t)
- override fun trace(marker: Marker, msg: String) = trace(msg)
- override fun trace(marker: Marker?, format: String, arg: Any?) = trace(format, arg)
- override fun trace(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
- trace(format, arg1, arg2)
-
- override fun trace(marker: Marker?, format: String, vararg arguments: Any?) =
- trace(format, *arguments)
-
- override fun trace(marker: Marker?, msg: String, t: Throwable?) = trace(msg, t)
-
- override fun debug(msg: String) = d(msg)
- override fun debug(format: String, arg: Any?) = d(format, null, arg)
- override fun debug(format: String, arg1: Any?, arg2: Any?) = d(format, null, arg1, arg2)
- override fun debug(format: String, vararg arguments: Any?) = d(format, null, *arguments)
- override fun debug(msg: String, t: Throwable?) = d(msg, t)
- override fun debug(marker: Marker, msg: String) = debug(msg)
- override fun debug(marker: Marker?, format: String, arg: Any?) = debug(format, arg)
- override fun debug(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
- debug(format, arg1, arg2)
-
- override fun debug(marker: Marker?, format: String, vararg arguments: Any?) =
- debug(format, *arguments)
-
- override fun debug(marker: Marker?, msg: String, t: Throwable?) = debug(msg, t)
-
- override fun info(msg: String) = i(msg)
- override fun info(format: String, arg: Any?) = i(format, null, arg)
- override fun info(format: String, arg1: Any?, arg2: Any?) = i(format, null, arg1, arg2)
- override fun info(format: String, vararg arguments: Any?) = i(format, null, *arguments)
- override fun info(msg: String, t: Throwable?) = i(msg, t)
- override fun info(marker: Marker, msg: String) = info(msg)
- override fun info(marker: Marker?, format: String, arg: Any?) = info(format, arg)
- override fun info(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
- info(format, arg1, arg2)
-
- override fun info(marker: Marker?, format: String, vararg arguments: Any?) =
- info(format, *arguments)
-
- override fun info(marker: Marker?, msg: String, t: Throwable?) = info(msg, t)
-
- override fun warn(msg: String) = w(msg)
- override fun warn(format: String, arg: Any?) = w(format, null, arg)
- override fun warn(format: String, arg1: Any?, arg2: Any?) = w(format, null, arg1, arg2)
- override fun warn(format: String, vararg arguments: Any?) = w(format, null, *arguments)
- override fun warn(msg: String, t: Throwable?) = w(msg, t)
- override fun warn(marker: Marker, msg: String) = warn(msg)
- override fun warn(marker: Marker?, format: String, arg: Any?) = warn(format, arg)
- override fun warn(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
- warn(format, arg1, arg2)
-
- override fun warn(marker: Marker?, format: String, vararg arguments: Any?) =
- warn(format, *arguments)
-
- override fun warn(marker: Marker?, msg: String, t: Throwable?) = warn(msg, t)
-
- override fun error(msg: String) = e(msg)
- override fun error(format: String, arg: Any?) = e(format, null, arg)
- override fun error(format: String, arg1: Any?, arg2: Any?) = e(format, null, arg1, arg2)
- override fun error(format: String, vararg arguments: Any?) = e(format, null, *arguments)
- override fun error(msg: String, t: Throwable?) = e(msg, t)
- override fun error(marker: Marker, msg: String) = error(msg)
- override fun error(marker: Marker?, format: String, arg: Any?) = error(format, arg)
- override fun error(marker: Marker?, format: String, arg1: Any?, arg2: Any?) =
- error(format, arg1, arg2)
-
- override fun error(marker: Marker?, format: String, vararg arguments: Any?) =
- error(format, *arguments)
-
- override fun error(marker: Marker?, msg: String, t: Throwable?) = error(msg, t)
-}
-
-object TimberLoggerFactory : LoggerFactory {
- private class TimberLogger(name: String) : AbstractLogger(name) {
-
- // We defer the log level checks to Timber.
- override fun isTraceEnabled() = true
- override fun isDebugEnabled() = true
- override fun isInfoEnabled() = true
- override fun isWarnEnabled() = true
- override fun isErrorEnabled() = true
-
- // Replace slf4j's "{}" format string style with standard Java's "%s".
- // The supposedly redundant escape on the } is not redundant.
- @Suppress("RegExpRedundantEscape")
- private fun String.fix() = replace("""(?!<\\)\{\}""".toRegex(), "%s")
-
- override fun t(message: String, t: Throwable?, vararg args: Any?) {
- Timber.tag(name).v(t, message.fix(), *args)
- }
-
- override fun d(message: String, t: Throwable?, vararg args: Any?) {
- Timber.tag(name).d(t, message.fix(), *args)
- }
-
- override fun i(message: String, t: Throwable?, vararg args: Any?) {
- Timber.tag(name).i(t, message.fix(), *args)
- }
-
- override fun w(message: String, t: Throwable?, vararg args: Any?) {
- Timber.tag(name).w(t, message.fix(), *args)
- }
-
- override fun e(message: String, t: Throwable?, vararg args: Any?) {
- Timber.tag(name).e(t, message.fix(), *args)
- }
- }
-
- override fun getLogger(name: String): Logger {
- return TimberLogger(name)
- }
-
- override fun getLogger(clazz: Class<*>): Logger {
- return TimberLogger(clazz.name)
- }
-
-}
-
-class SshjConfig : ConfigImpl() {
-
- init {
- loggerFactory = TimberLoggerFactory
- keepAliveProvider = KeepAliveProvider.HEARTBEAT
- version = "OpenSSH_8.2p1 Ubuntu-4ubuntu0.1"
-
- initKeyExchangeFactories()
- initKeyAlgorithms()
- initRandomFactory()
- initFileKeyProviderFactories()
- initCipherFactories()
- initCompressionFactories()
- initMACFactories()
- }
-
- private fun initKeyExchangeFactories() {
- keyExchangeFactories = listOf(
- Curve25519SHA256.Factory(),
- FactoryLibSsh(),
- ECDHNistP.Factory521(),
- ECDHNistP.Factory384(),
- ECDHNistP.Factory256(),
- DHGexSHA256.Factory(),
- // Sends "ext-info-c" with the list of key exchange algorithms. This is needed to get
- // rsa-sha2-* key types to work with some servers (e.g. GitHub).
- ExtInfoClientFactory(),
- )
- }
-
- private fun initKeyAlgorithms() {
- keyAlgorithms = listOf(
- KeyAlgorithms.SSHRSACertV01(),
- KeyAlgorithms.EdDSA25519(),
- KeyAlgorithms.RSASHA512(),
- KeyAlgorithms.RSASHA256(),
- KeyAlgorithms.ECDSASHANistp521(),
- KeyAlgorithms.ECDSASHANistp384(),
- KeyAlgorithms.ECDSASHANistp256(),
- KeyAlgorithms.SSHRSA(),
- )
- }
-
- private fun initRandomFactory() {
- randomFactory = SingletonRandomFactory(JCERandom.Factory())
- }
-
- private fun initFileKeyProviderFactories() {
- fileKeyProviderFactories = listOf(
- OpenSSHKeyV1KeyFile.Factory(),
- PKCS8KeyFile.Factory(),
- PKCS5KeyFile.Factory(),
- OpenSSHKeyFile.Factory(),
- PuTTYKeyFile.Factory(),
- )
- }
-
-
- private fun initCipherFactories() {
- cipherFactories = listOf(
- BlockCiphers.AES256CTR(),
- BlockCiphers.AES192CTR(),
- BlockCiphers.AES128CTR(),
- )
- }
-
- private fun initMACFactories() {
- macFactories = listOf(
- Macs.HMACSHA2512Etm(),
- Macs.HMACSHA2256Etm(),
- Macs.HMACSHA2512(),
- Macs.HMACSHA2256(),
- )
- }
-
- private fun initCompressionFactories() {
- compressionFactories = listOf(
- NoneCompression.Factory(),
- )
- }
-}
diff --git a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt b/app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt
deleted file mode 100644
index 85b5f753..00000000
--- a/app/src/main/java/com/zeapo/pwdstore/git/config/SshjSessionFactory.kt
+++ /dev/null
@@ -1,180 +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.util.Base64
-import com.github.ajalt.timberkt.d
-import com.github.ajalt.timberkt.w
-import java.io.File
-import java.io.IOException
-import java.io.InputStream
-import java.io.OutputStream
-import java.security.GeneralSecurityException
-import java.util.concurrent.TimeUnit
-import kotlin.coroutines.Continuation
-import kotlin.coroutines.suspendCoroutine
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.runBlocking
-import net.schmizz.sshj.SSHClient
-import net.schmizz.sshj.common.Buffer.PlainBuffer
-import net.schmizz.sshj.common.DisconnectReason
-import net.schmizz.sshj.common.SSHException
-import net.schmizz.sshj.common.SSHRuntimeException
-import net.schmizz.sshj.common.SecurityUtils
-import net.schmizz.sshj.connection.channel.direct.Session
-import net.schmizz.sshj.transport.verification.FingerprintVerifier
-import net.schmizz.sshj.transport.verification.HostKeyVerifier
-import net.schmizz.sshj.userauth.password.PasswordFinder
-import net.schmizz.sshj.userauth.password.Resource
-import org.eclipse.jgit.transport.CredentialsProvider
-import org.eclipse.jgit.transport.RemoteSession
-import org.eclipse.jgit.transport.SshSessionFactory
-import org.eclipse.jgit.transport.URIish
-import org.eclipse.jgit.util.FS
-
-sealed class SshAuthData {
- class Password(val passwordFinder: InteractivePasswordFinder) : SshAuthData()
- class PublicKeyFile(val keyFile: File, val passphraseFinder: InteractivePasswordFinder) : SshAuthData()
-}
-
-abstract class InteractivePasswordFinder : PasswordFinder {
-
- private var isRetry = false
-
- abstract fun askForPassword(cont: Continuation<String?>, isRetry: Boolean)
-
- final override fun reqPassword(resource: Resource<*>?): CharArray {
- val password = runBlocking(Dispatchers.Main) {
- suspendCoroutine<String?> { cont ->
- askForPassword(cont, isRetry)
- }
- }
- isRetry = true
- return password?.toCharArray()
- ?: throw SSHException(DisconnectReason.AUTH_CANCELLED_BY_USER)
- }
-
- final override fun shouldRetry(resource: Resource<*>?) = true
-}
-
-class SshjSessionFactory(private val authData: SshAuthData, private val hostKeyFile: File) : SshSessionFactory() {
-
- private var currentSession: SshjSession? = null
-
- override fun getSession(uri: URIish, credentialsProvider: CredentialsProvider?, fs: FS?, tms: Int): RemoteSession {
- return currentSession ?: SshjSession(uri, uri.user, authData, hostKeyFile).connect().also {
- d { "New SSH connection created" }
- currentSession = it
- }
- }
-
- fun close() {
- currentSession?.close()
- }
-}
-
-private fun makeTofuHostKeyVerifier(hostKeyFile: File): HostKeyVerifier {
- if (!hostKeyFile.exists()) {
- return HostKeyVerifier { _, _, key ->
- val digest = try {
- SecurityUtils.getMessageDigest("SHA-256")
- } catch (e: GeneralSecurityException) {
- throw SSHRuntimeException(e)
- }
- digest.update(PlainBuffer().putPublicKey(key).compactData)
- val digestData = digest.digest()
- val hostKeyEntry = "SHA256:${Base64.encodeToString(digestData, Base64.NO_WRAP)}"
- d { "Trusting host key on first use: $hostKeyEntry" }
- hostKeyFile.writeText(hostKeyEntry)
- true
- }
- } else {
- val hostKeyEntry = hostKeyFile.readText()
- d { "Pinned host key: $hostKeyEntry" }
- return FingerprintVerifier.getInstance(hostKeyEntry)
- }
-}
-
-private class SshjSession(uri: URIish, private val username: String, private val authData: SshAuthData, private val hostKeyFile: File) : RemoteSession {
-
- private lateinit var ssh: SSHClient
- private var currentCommand: Session? = null
-
- private val uri = if (uri.host.contains('@')) {
- // URIish's String constructor cannot handle '@' in the user part of the URI and the URL
- // constructor can't be used since Java's URL does not recognize the ssh scheme. We thus
- // need to patch everything up ourselves.
- d { "Before fixup: user=${uri.user}, host=${uri.host}" }
- val userPlusHost = "${uri.user}@${uri.host}"
- val realUser = userPlusHost.substringBeforeLast('@')
- val realHost = userPlusHost.substringAfterLast('@')
- uri.setUser(realUser).setHost(realHost).also { d { "After fixup: user=${it.user}, host=${it.host}" } }
- } else {
- uri
- }
-
- fun connect(): SshjSession {
- ssh = SSHClient(SshjConfig())
- ssh.addHostKeyVerifier(makeTofuHostKeyVerifier(hostKeyFile))
- ssh.connect(uri.host, uri.port.takeUnless { it == -1 } ?: 22)
- if (!ssh.isConnected)
- throw IOException()
- when (authData) {
- is SshAuthData.Password -> {
- ssh.authPassword(username, authData.passwordFinder)
- }
- is SshAuthData.PublicKeyFile -> {
- ssh.authPublickey(username, ssh.loadKeys(authData.keyFile.absolutePath, authData.passphraseFinder))
- }
- }
- return this
- }
-
- override fun exec(commandName: String?, timeout: Int): Process {
- if (currentCommand != null) {
- w { "Killing old command" }
- disconnect()
- }
- val session = ssh.startSession()
- currentCommand = session
- return SshjProcess(session.exec(commandName), timeout.toLong())
- }
-
- /**
- * Kills the current command if one is running and returns the session into a state where `exec`
- * can be called.
- *
- * Note that this does *not* disconnect the session. Unfortunately, the function has to be
- * called `disconnect` to override the corresponding abstract function in `RemoteSession`.
- */
- override fun disconnect() {
- currentCommand?.close()
- currentCommand = null
- }
-
- fun close() {
- disconnect()
- ssh.close()
- }
-}
-
-private class SshjProcess(private val command: Session.Command, private val timeout: Long) : Process() {
-
- override fun waitFor(): Int {
- command.join(timeout, TimeUnit.SECONDS)
- command.close()
- return exitValue()
- }
-
- override fun destroy() = command.close()
-
- override fun getOutputStream(): OutputStream = command.outputStream
-
- override fun getErrorStream(): InputStream = command.errorStream
-
- override fun exitValue(): Int = command.exitStatus
-
- override fun getInputStream(): InputStream = command.inputStream
-}