diff options
Diffstat (limited to 'app/src/main/java/com/zeapo/pwdstore/git/config')
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 -} |
