diff --git a/jgit-gpg-bc/build.gradle.kts b/jgit-gpg-bc/build.gradle.kts index 41b566e40..5d33ea5fe 100644 --- a/jgit-gpg-bc/build.gradle.kts +++ b/jgit-gpg-bc/build.gradle.kts @@ -2,14 +2,14 @@ plugins { id("org.openrewrite.build.language-library") } -val bcVersion = "1.70" +val bcVersion = "1.85" +val bcProvVersion = "1.85.2" dependencies { implementation(project(":jgit")) compileOnly("org.slf4j:slf4j-api:1.7.36") - implementation("org.bouncycastle:bcpg-jdk15on:$bcVersion") - implementation("org.bouncycastle:bcprov-jdk15on:$bcVersion") - implementation("org.bouncycastle:bcutil-jdk15on:$bcVersion") - implementation("org.bouncycastle:bcpkix-jdk15on:$bcVersion") + implementation("org.bouncycastle:bcpg-jdk18on:$bcVersion") + implementation("org.bouncycastle:bcprov-jdk18on:$bcProvVersion") + implementation("org.bouncycastle:bcutil-jdk18on:$bcVersion") } java { diff --git a/jgit-gpg-bc/src/main/java/org/openrewrite/jgit/gpg/bc/internal/BCText.java b/jgit-gpg-bc/src/main/java/org/openrewrite/jgit/gpg/bc/internal/BCText.java index 894f4c655..13183b18b 100644 --- a/jgit-gpg-bc/src/main/java/org/openrewrite/jgit/gpg/bc/internal/BCText.java +++ b/jgit-gpg-bc/src/main/java/org/openrewrite/jgit/gpg/bc/internal/BCText.java @@ -29,6 +29,7 @@ public static BCText get() { // @formatter:off /***/ public String corrupt25519Key; /***/ public String credentialPassphrase; + /***/ public String cryptAeadUnsupported; /***/ public String cryptCipherError; /***/ public String cryptWrongDecryptedLength; /***/ public String gpgFailedToParseSecretKey; diff --git a/jgit-gpg-bc/src/main/java/org/openrewrite/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java b/jgit-gpg-bc/src/main/java/org/openrewrite/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java index 7589e25a8..678022da2 100644 --- a/jgit-gpg-bc/src/main/java/org/openrewrite/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java +++ b/jgit-gpg-bc/src/main/java/org/openrewrite/jgit/gpg/bc/internal/keys/OCBPBEProtectionRemoverFactory.java @@ -67,6 +67,15 @@ public PBESecretKeyDecryptor createDecryptor(String protection) throws PGPException { return new PBESecretKeyDecryptor(passphrase, calculatorProvider) { + @Override + public byte[] recoverKeyData(int encAlgorithm, int aeadAlgorithm, + byte[] key, byte[] iv, int packetTag, int keyVersion, + byte[] keyData, byte[] pubkeyData) throws PGPException { + // The AAD is fixed at factory level, so this decryptor cannot + // serve the v6 key format, which derives it per key. + throw new PGPException(BCText.get().cryptAeadUnsupported); + } + @Override public byte[] recoverKeyData(int encAlgorithm, byte[] key, byte[] iv, byte[] encrypted, int encryptedOffset, diff --git a/jgit-gpg-bc/src/main/resources/org/openrewrite/jgit/gpg/bc/internal/BCText.properties b/jgit-gpg-bc/src/main/resources/org/openrewrite/jgit/gpg/bc/internal/BCText.properties index cc807b603..b10acb6b1 100644 --- a/jgit-gpg-bc/src/main/resources/org/openrewrite/jgit/gpg/bc/internal/BCText.properties +++ b/jgit-gpg-bc/src/main/resources/org/openrewrite/jgit/gpg/bc/internal/BCText.properties @@ -40,6 +40,7 @@ corrupt25519Key=Ed25519/Curve25519 public key has wrong length: {0} credentialPassphrase=Passphrase +cryptAeadUnsupported=Cannot decrypt AEAD-protected secret keys in the OpenPGP v6 key format cryptCipherError=Cannot create cipher to decrypt: {0} cryptWrongDecryptedLength=Decrypted key has wrong length; expected {0} bytes, got only {1} bytes gpgFailedToParseSecretKey=Failed to parse secret key file {0}. Is the entered passphrase correct? diff --git a/jgit-ssh-jsch/README.md b/jgit-ssh-jsch/README.md deleted file mode 100644 index fcb626f77..000000000 --- a/jgit-ssh-jsch/README.md +++ /dev/null @@ -1 +0,0 @@ -Fork of org.eclipse.jgit.ssh.jsch stable-5.13 with com.github.mwiede:jsch for support of id\_ecdsa, id\_ed25519 and id\_ed448 SSH key types. diff --git a/jgit-ssh-jsch/build.gradle.kts b/jgit-ssh-jsch/build.gradle.kts deleted file mode 100644 index 41917430d..000000000 --- a/jgit-ssh-jsch/build.gradle.kts +++ /dev/null @@ -1,28 +0,0 @@ -plugins { - id("org.openrewrite.build.language-library") -} - -dependencies { - implementation(project(":jgit")) - implementation("com.github.mwiede:jsch:0.2.17") - compileOnly("org.slf4j:slf4j-api:1.7.36") -} - -java { - toolchain { - languageVersion.set(JavaLanguageVersion.of(8)) - } -} - -tasks.withType().configureEach { - options.isFork = true - options.release.set(null as? Int?) // remove `--release 8` set in `org.openrewrite.java-base` -} - -tasks.named("javadoc") { - enabled = false -} - -tasks.named("licenseMain") { - enabled = false -} diff --git a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/internal/transport/jsch/JSchText.java b/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/internal/transport/jsch/JSchText.java deleted file mode 100644 index 064a954c9..000000000 --- a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/internal/transport/jsch/JSchText.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2020, Michael Dardis and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ -package org.openrewrite.jgit.internal.transport.jsch; - -import org.openrewrite.jgit.nls.NLS; -import org.openrewrite.jgit.nls.TranslationBundle; - -/** - * Externalized text messages for localization. - */ -public final class JSchText extends TranslationBundle { - - /** - * Get an instance of this translation bundle. - * - * @return an instance of this translation bundle - */ - public static JSchText get() { - return NLS.getBundleFor(JSchText.class); - } - - // @formatter:off - /***/ public String connectionFailed; - /***/ public String sshUserNameError; - /***/ public String transportSSHRetryInterrupt; - /***/ public String unknownHost; - -} diff --git a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/CredentialsProviderUserInfo.java b/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/CredentialsProviderUserInfo.java deleted file mode 100644 index a82512d12..000000000 --- a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/CredentialsProviderUserInfo.java +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Copyright (C) 2010, Google Inc. and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -//TODO(ms): move to org.openrewrite.jgit.ssh.jsch in 6.0 -package org.openrewrite.jgit.transport; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import com.jcraft.jsch.Session; -import com.jcraft.jsch.UIKeyboardInteractive; -import com.jcraft.jsch.UserInfo; - -/** - * A JSch {@link com.jcraft.jsch.UserInfo} adapter for a - * {@link org.openrewrite.jgit.transport.CredentialsProvider}. - */ -public class CredentialsProviderUserInfo implements UserInfo, - UIKeyboardInteractive { - private final URIish uri; - - private final CredentialsProvider provider; - - private String password; - - private String passphrase; - - /** - * Wrap a CredentialsProvider to make it suitable for use with JSch. - * - * @param session - * the JSch session this UserInfo will support authentication on. - * @param credentialsProvider - * the provider that will perform the authentication. - */ - public CredentialsProviderUserInfo(Session session, - CredentialsProvider credentialsProvider) { - this.uri = createURI(session); - this.provider = credentialsProvider; - } - - private static URIish createURI(Session session) { - URIish uri = new URIish(); - uri = uri.setScheme("ssh"); //$NON-NLS-1$ - uri = uri.setUser(session.getUserName()); - uri = uri.setHost(session.getHost()); - uri = uri.setPort(session.getPort()); - return uri; - } - - /** {@inheritDoc} */ - @Override - public String getPassword() { - return password; - } - - /** {@inheritDoc} */ - @Override - public String getPassphrase() { - return passphrase; - } - - /** {@inheritDoc} */ - @Override - public boolean promptPassphrase(String msg) { - CredentialItem.StringType v = newPrompt(msg); - if (provider.get(uri, v)) { - passphrase = v.getValue(); - return true; - } - passphrase = null; - return false; - } - - /** {@inheritDoc} */ - @Override - public boolean promptPassword(String msg) { - CredentialItem.Password p = new CredentialItem.Password(msg); - if (provider.get(uri, p)) { - password = new String(p.getValue()); - return true; - } - password = null; - return false; - } - - private CredentialItem.StringType newPrompt(String msg) { - return new CredentialItem.StringType(msg, true); - } - - /** {@inheritDoc} */ - @Override - public boolean promptYesNo(String msg) { - CredentialItem.YesNoType v = new CredentialItem.YesNoType(msg); - return provider.get(uri, v) && v.getValue(); - } - - /** {@inheritDoc} */ - @Override - public void showMessage(String msg) { - provider.get(uri, new CredentialItem.InformationalMessage(msg)); - } - - /** {@inheritDoc} */ - @Override - public String[] promptKeyboardInteractive(String destination, String name, - String instruction, String[] prompt, boolean[] echo) { - CredentialItem.StringType[] v = new CredentialItem.StringType[prompt.length]; - for (int i = 0; i < prompt.length; i++) - v[i] = new CredentialItem.StringType(prompt[i], !echo[i]); - - List items = new ArrayList<>(); - if (instruction != null && instruction.length() > 0) - items.add(new CredentialItem.InformationalMessage(instruction)); - items.addAll(Arrays.asList(v)); - - if (!provider.get(uri, items)) - return null; // cancel - - String[] result = new String[v.length]; - for (int i = 0; i < v.length; i++) - result[i] = v[i].getValue(); - return result; - } -} diff --git a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/JschConfigSessionFactory.java b/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/JschConfigSessionFactory.java deleted file mode 100644 index 0ac81bb31..000000000 --- a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/JschConfigSessionFactory.java +++ /dev/null @@ -1,557 +0,0 @@ -/* - * Copyright (C) 2018, Sasa Zivkov - * Copyright (C) 2016, Mark Ingram - * Copyright (C) 2009, Constantine Plotnikov - * Copyright (C) 2008-2009, Google Inc. - * Copyright (C) 2009, Google, Inc. - * Copyright (C) 2009, JetBrains s.r.o. - * Copyright (C) 2008, Robin Rosenberg - * Copyright (C) 2008, Shawn O. Pearce and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -//TODO(ms): move to org.openrewrite.jgit.ssh.jsch in 6.0 -package org.openrewrite.jgit.transport; - -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.net.ConnectException; -import java.net.UnknownHostException; -import java.text.MessageFormat; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.stream.Stream; - -import org.openrewrite.jgit.errors.TransportException; -import org.openrewrite.jgit.internal.transport.jsch.JSchText; -import org.openrewrite.jgit.util.FS; -import org.openrewrite.jgit.util.StringUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.jcraft.jsch.ConfigRepository; -import com.jcraft.jsch.ConfigRepository.Config; -import com.jcraft.jsch.HostKey; -import com.jcraft.jsch.HostKeyRepository; -import com.jcraft.jsch.JSch; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; - -/** - * The base session factory that loads known hosts and private keys from - * $HOME/.ssh. - *

- * This is the default implementation used by JGit and provides most of the - * compatibility necessary to match OpenSSH, a popular implementation of SSH - * used by C Git. - *

- * The factory does not provide UI behavior. Override the method - * {@link #configure(org.openrewrite.jgit.transport.OpenSshConfig.Host, Session)} to - * supply appropriate {@link com.jcraft.jsch.UserInfo} to the session. - */ -public class JschConfigSessionFactory extends SshSessionFactory { - - private static final String JSCH = "jsch"; //$NON-NLS-1$ - - private static final Logger LOG = LoggerFactory - .getLogger(JschConfigSessionFactory.class); - - /** - * We use different Jsch instances for hosts that have an IdentityFile - * configured in ~/.ssh/config. Jsch by default would cache decrypted keys - * only per session, which results in repeated password prompts. Using - * different Jsch instances, we can cache the keys on these instances so - * that they will be re-used for successive sessions, and thus the user is - * prompted for a key password only once while Eclipse runs. - */ - private final Map byIdentityFile = new HashMap<>(); - - private JSch defaultJSch; - - private OpenSshConfig config; - - /** {@inheritDoc} */ - @Override - public synchronized RemoteSession getSession(URIish uri, - CredentialsProvider credentialsProvider, FS fs, int tms) - throws TransportException { - - String user = uri.getUser(); - final String pass = uri.getPass(); - String host = uri.getHost(); - int port = uri.getPort(); - - try { - if (config == null) - config = OpenSshConfig.get(fs); - - final OpenSshConfig.Host hc = config.lookup(host); - if (port <= 0) - port = hc.getPort(); - if (user == null) - user = hc.getUser(); - - Session session = createSession(credentialsProvider, fs, user, - pass, host, port, hc); - - int retries = 0; - while (!session.isConnected()) { - try { - retries++; - session.connect(tms); - } catch (JSchException e) { - session.disconnect(); - session = null; - // Make sure our known_hosts is not outdated - knownHosts(getJSch(hc, fs), fs); - - if (isAuthenticationCanceled(e)) { - throw e; - } else if (isAuthenticationFailed(e) - && credentialsProvider != null) { - // if authentication failed maybe credentials changed at - // the remote end therefore reset credentials and retry - if (retries < 3) { - credentialsProvider.reset(uri); - session = createSession(credentialsProvider, fs, - user, pass, host, port, hc); - } else - throw e; - } else if (retries >= hc.getConnectionAttempts()) { - throw e; - } else { - try { - Thread.sleep(1000); - session = createSession(credentialsProvider, fs, - user, pass, host, port, hc); - } catch (InterruptedException e1) { - throw new TransportException( - JSchText.get().transportSSHRetryInterrupt, - e1); - } - } - } - } - - return new JschSession(session, uri); - - } catch (JSchException je) { - final Throwable c = je.getCause(); - if (c instanceof UnknownHostException) { - throw new TransportException(uri, - JSchText.get().unknownHost, - je); - } - if (c instanceof ConnectException) { - throw new TransportException(uri, c.getMessage(), je); - } - throw new TransportException(uri, je.getMessage(), je); - } - - } - - @Override - public String getType() { - return JSCH; - } - - private static boolean isAuthenticationFailed(JSchException e) { - return e.getCause() == null && e.getMessage().equals("Auth fail"); //$NON-NLS-1$ - } - - private static boolean isAuthenticationCanceled(JSchException e) { - return e.getCause() == null && e.getMessage().equals("Auth cancel"); //$NON-NLS-1$ - } - - // Package visibility for tests - Session createSession(CredentialsProvider credentialsProvider, - FS fs, String user, final String pass, String host, int port, - final OpenSshConfig.Host hc) throws JSchException { - final Session session = createSession(hc, user, host, port, fs); - // Jsch will have overridden the explicit user by the one from the SSH - // config file... - setUserName(session, user); - // Jsch will also have overridden the port. - if (port > 0 && port != session.getPort()) { - session.setPort(port); - } - // We retry already in getSession() method. JSch must not retry - // on its own. - session.setConfig("MaxAuthTries", "1"); //$NON-NLS-1$ //$NON-NLS-2$ - if (pass != null) - session.setPassword(pass); - final String strictHostKeyCheckingPolicy = hc - .getStrictHostKeyChecking(); - if (strictHostKeyCheckingPolicy != null) - session.setConfig("StrictHostKeyChecking", //$NON-NLS-1$ - strictHostKeyCheckingPolicy); - final String pauth = hc.getPreferredAuthentications(); - if (pauth != null) - session.setConfig("PreferredAuthentications", pauth); //$NON-NLS-1$ - if (credentialsProvider != null - && (!hc.isBatchMode() || !credentialsProvider.isInteractive())) { - session.setUserInfo(new CredentialsProviderUserInfo(session, - credentialsProvider)); - } - safeConfig(session, hc.getConfig()); - if (hc.getConfig().getValue("HostKeyAlgorithms") == null) { //$NON-NLS-1$ - setPreferredKeyTypesOrder(session); - } - configure(hc, session); - return session; - } - - private void safeConfig(Session session, Config cfg) { - // Ensure that Jsch checks all configured algorithms, not just its - // built-in ones. Otherwise it may propose an algorithm for which it - // doesn't have an implementation, and then run into an NPE if that - // algorithm ends up being chosen. - copyConfigValueToSession(session, cfg, "Ciphers", "CheckCiphers"); //$NON-NLS-1$ //$NON-NLS-2$ - copyConfigValueToSession(session, cfg, "KexAlgorithms", "CheckKexes"); //$NON-NLS-1$ //$NON-NLS-2$ - copyConfigValueToSession(session, cfg, "HostKeyAlgorithms", //$NON-NLS-1$ - "CheckSignatures"); //$NON-NLS-1$ - } - - private static void setPreferredKeyTypesOrder(Session session) { - HostKeyRepository hkr = session.getHostKeyRepository(); - HostKey[] hostKeys = hkr.getHostKey(hostName(session), null); - - if (hostKeys == null) { - return; - } - - List known = Stream.of(hostKeys) - .map(HostKey::getType) - .collect(toList()); - - if (!known.isEmpty()) { - String serverHostKey = "server_host_key"; //$NON-NLS-1$ - String current = session.getConfig(serverHostKey); - if (current == null) { - session.setConfig(serverHostKey, String.join(",", known)); //$NON-NLS-1$ - return; - } - - String knownFirst = Stream.concat( - known.stream(), - Stream.of(current.split(",")) //$NON-NLS-1$ - .filter(s -> !known.contains(s))) - .collect(joining(",")); //$NON-NLS-1$ - session.setConfig(serverHostKey, knownFirst); - } - } - - private static String hostName(Session s) { - if (s.getPort() == SshConstants.SSH_DEFAULT_PORT) { - return s.getHost(); - } - return String.format("[%s]:%d", s.getHost(), //$NON-NLS-1$ - Integer.valueOf(s.getPort())); - } - - private void copyConfigValueToSession(Session session, Config cfg, - String from, String to) { - String value = cfg.getValue(from); - if (value != null) { - session.setConfig(to, value); - } - } - - private void setUserName(Session session, String userName) { - // Jsch 0.1.54 picks up the user name from the ssh config, even if an - // explicit user name was given! We must correct that if ~/.ssh/config - // has a different user name. - if (userName == null || userName.isEmpty() - || userName.equals(session.getUserName())) { - return; - } - try { - Class[] parameterTypes = { String.class }; - Method method = Session.class.getDeclaredMethod("setUserName", //$NON-NLS-1$ - parameterTypes); - method.setAccessible(true); - method.invoke(session, userName); - } catch (NullPointerException | IllegalAccessException - | IllegalArgumentException | InvocationTargetException - | NoSuchMethodException | SecurityException e) { - LOG.error(MessageFormat.format(JSchText.get().sshUserNameError, - userName, session.getUserName()), e); - } - } - - /** - * Create a new remote session for the requested address. - * - * @param hc - * host configuration - * @param user - * login to authenticate as. - * @param host - * server name to connect to. - * @param port - * port number of the SSH daemon (typically 22). - * @param fs - * the file system abstraction which will be necessary to - * perform certain file system operations. - * @return new session instance, but otherwise unconfigured. - * @throws com.jcraft.jsch.JSchException - * the session could not be created. - */ - protected Session createSession(final OpenSshConfig.Host hc, - final String user, final String host, final int port, FS fs) - throws JSchException { - return getJSch(hc, fs).getSession(user, host, port); - } - - /** - * Provide additional configuration for the JSch instance. This method could - * be overridden to supply a preferred - * {@link com.jcraft.jsch.IdentityRepository}. - * - * @param jsch - * jsch instance - * @since 4.5 - */ - protected void configureJSch(JSch jsch) { - // No additional configuration required. - } - - /** - * Provide additional configuration for the session based on the host - * information. This method could be used to supply - * {@link com.jcraft.jsch.UserInfo}. - * - * @param hc - * host configuration - * @param session - * session to configure - */ - protected void configure(OpenSshConfig.Host hc, Session session) { - // No additional configuration required. - } - - /** - * Obtain the JSch used to create new sessions. - * - * @param hc - * host configuration - * @param fs - * the file system abstraction which will be necessary to - * perform certain file system operations. - * @return the JSch instance to use. - * @throws com.jcraft.jsch.JSchException - * the user configuration could not be created. - */ - protected JSch getJSch(OpenSshConfig.Host hc, FS fs) throws JSchException { - if (defaultJSch == null) { - defaultJSch = createDefaultJSch(fs); - if (defaultJSch.getConfigRepository() == null) { - defaultJSch.setConfigRepository( - new JschBugFixingConfigRepository(config)); - } - for (Object name : defaultJSch.getIdentityNames()) - byIdentityFile.put((String) name, defaultJSch); - } - - final File identityFile = hc.getIdentityFile(); - if (identityFile == null) - return defaultJSch; - - final String identityKey = identityFile.getAbsolutePath(); - JSch jsch = byIdentityFile.get(identityKey); - if (jsch == null) { - jsch = new JSch(); - configureJSch(jsch); - if (jsch.getConfigRepository() == null) { - jsch.setConfigRepository(defaultJSch.getConfigRepository()); - } - jsch.setHostKeyRepository(defaultJSch.getHostKeyRepository()); - jsch.addIdentity(identityKey); - byIdentityFile.put(identityKey, jsch); - } - return jsch; - } - - /** - * Create default instance of jsch - * - * @param fs - * the file system abstraction which will be necessary to perform - * certain file system operations. - * @return the new default JSch implementation. - * @throws com.jcraft.jsch.JSchException - * known host keys cannot be loaded. - */ - protected JSch createDefaultJSch(FS fs) throws JSchException { - final JSch jsch = new JSch(); - // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=537790 and - // https://bugs.eclipse.org/bugs/show_bug.cgi?id=576604 - copyGlobalConfigIfNotSet("signature.rsa", "ssh-rsa"); //$NON-NLS-1$ //$NON-NLS-2$ - copyGlobalConfigIfNotSet("signature.dss", "ssh-dss"); //$NON-NLS-1$ //$NON-NLS-2$ - configureJSch(jsch); - knownHosts(jsch, fs); - identities(jsch, fs); - return jsch; - } - - private void copyGlobalConfigIfNotSet(String from, String to) { - String toValue = JSch.getConfig(to); - if (StringUtils.isEmptyOrNull(toValue)) { - String fromValue = JSch.getConfig(from); - if (!StringUtils.isEmptyOrNull(fromValue)) { - JSch.setConfig(to, fromValue); - } - } - } - - private static void knownHosts(JSch sch, FS fs) throws JSchException { - final File home = fs.userHome(); - if (home == null) - return; - final File known_hosts = new File(new File(home, ".ssh"), "known_hosts"); //$NON-NLS-1$ //$NON-NLS-2$ - try (FileInputStream in = new FileInputStream(known_hosts)) { - sch.setKnownHosts(in); - } catch (FileNotFoundException none) { - // Oh well. They don't have a known hosts in home. - } catch (IOException err) { - // Oh well. They don't have a known hosts in home. - } - } - - private static void identities(JSch sch, FS fs) { - final File home = fs.userHome(); - if (home == null) - return; - final File sshdir = new File(home, ".ssh"); //$NON-NLS-1$ - if (sshdir.isDirectory()) { - loadIdentity(sch, new File(sshdir, "identity")); //$NON-NLS-1$ - loadIdentity(sch, new File(sshdir, "id_rsa")); //$NON-NLS-1$ - loadIdentity(sch, new File(sshdir, "id_dsa")); //$NON-NLS-1$ - loadIdentity(sch, new File(sshdir, "id_ecdsa")); //$NON-NLS-1$ - loadIdentity(sch, new File(sshdir, "id_ed25519")); //$NON-NLS-1$ - loadIdentity(sch, new File(sshdir, "id_ed448")); //$NON-NLS-1$ - } - } - - private static void loadIdentity(JSch sch, File priv) { - if (priv.isFile()) { - try { - sch.addIdentity(priv.getAbsolutePath()); - } catch (JSchException e) { - // Instead, pretend the key doesn't exist. - } - } - } - - private static class JschBugFixingConfigRepository - implements ConfigRepository { - - private final ConfigRepository base; - - public JschBugFixingConfigRepository(ConfigRepository base) { - this.base = base; - } - - @Override - public Config getConfig(String host) { - return new JschBugFixingConfig(base.getConfig(host)); - } - - /** - * A {@link com.jcraft.jsch.ConfigRepository.Config} that transforms - * some values from the config file into the format Jsch 0.1.54 expects. - * This is a work-around for bugs in Jsch. - *

- * Additionally, this config hides the IdentityFile config entries from - * Jsch; we manage those ourselves. Otherwise Jsch would cache passwords - * (or rather, decrypted keys) only for a single session, resulting in - * multiple password prompts for user operations that use several Jsch - * sessions. - */ - private static class JschBugFixingConfig implements Config { - - private static final String[] NO_IDENTITIES = {}; - - private final Config real; - - public JschBugFixingConfig(Config delegate) { - real = delegate; - } - - @Override - public String getHostname() { - return real.getHostname(); - } - - @Override - public String getUser() { - return real.getUser(); - } - - @Override - public int getPort() { - return real.getPort(); - } - - @Override - public String getValue(String key) { - String k = key.toUpperCase(Locale.ROOT); - if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$ - return null; - } - String result = real.getValue(key); - if (result != null) { - if ("SERVERALIVEINTERVAL".equals(k) //$NON-NLS-1$ - || "CONNECTTIMEOUT".equals(k)) { //$NON-NLS-1$ - // These values are in seconds. Jsch 0.1.54 passes them - // on as is to java.net.Socket.setSoTimeout(), which - // expects milliseconds. So convert here to - // milliseconds. - try { - int timeout = Integer.parseInt(result); - result = Long.toString( - TimeUnit.SECONDS.toMillis(timeout)); - } catch (NumberFormatException e) { - // Ignore - } - } - } - return result; - } - - @Override - public String[] getValues(String key) { - String k = key.toUpperCase(Locale.ROOT); - if ("IDENTITYFILE".equals(k)) { //$NON-NLS-1$ - return NO_IDENTITIES; - } - return real.getValues(key); - } - } - } - - /** - * Set the {@link OpenSshConfig} to use. Intended for use in tests. - * - * @param config - * to use - */ - synchronized void setConfig(OpenSshConfig config) { - this.config = config; - } -} diff --git a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/JschSession.java b/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/JschSession.java deleted file mode 100644 index f8422b91b..000000000 --- a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/JschSession.java +++ /dev/null @@ -1,390 +0,0 @@ -/* - * Copyright (C) 2009, Constantine Plotnikov - * Copyright (C) 2008-2009, Google Inc. - * Copyright (C) 2009, Google, Inc. - * Copyright (C) 2009, JetBrains s.r.o. - * Copyright (C) 2008, Robin Rosenberg - * Copyright (C) 2008, Shawn O. Pearce and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -//TODO(ms): move to org.openrewrite.jgit.ssh.jsch in 6.0 -package org.openrewrite.jgit.transport; - -import java.io.BufferedOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.Callable; -import java.util.concurrent.TimeUnit; - -import org.openrewrite.jgit.errors.TransportException; -import org.openrewrite.jgit.internal.transport.jsch.JSchText; -import org.openrewrite.jgit.util.io.IsolatedOutputStream; - -import com.jcraft.jsch.Channel; -import com.jcraft.jsch.ChannelExec; -import com.jcraft.jsch.ChannelSftp; -import com.jcraft.jsch.JSchException; -import com.jcraft.jsch.Session; -import com.jcraft.jsch.SftpException; - -/** - * Run remote commands using Jsch. - *

- * This class is the default session implementation using Jsch. Note that - * {@link org.openrewrite.jgit.transport.JschConfigSessionFactory} is used to create - * the actual session passed to the constructor. - */ -public class JschSession implements RemoteSession2 { - final Session sock; - final URIish uri; - - /** - * Create a new session object by passing the real Jsch session and the URI - * information. - * - * @param session - * the real Jsch session created elsewhere. - * @param uri - * the URI information for the remote connection - */ - public JschSession(Session session, URIish uri) { - sock = session; - this.uri = uri; - } - - /** {@inheritDoc} */ - @Override - public Process exec(String command, int timeout) throws IOException { - return exec(command, Collections.emptyMap(), timeout); - } - - /** {@inheritDoc} */ - @Override - public Process exec(String command, Map environment, - int timeout) throws IOException { - return new JschProcess(command, environment, timeout); - } - - /** {@inheritDoc} */ - @Override - public void disconnect() { - if (sock.isConnected()) - sock.disconnect(); - } - - /** - * A kludge to allow {@link org.openrewrite.jgit.transport.TransportSftp} to get - * an Sftp channel from Jsch. Ideally, this method would be generic, which - * would require implementing generic Sftp channel operations in the - * RemoteSession class. - * - * @return a channel suitable for Sftp operations. - * @throws com.jcraft.jsch.JSchException - * on problems getting the channel. - * @deprecated since 5.2; use {@link #getFtpChannel()} instead - */ - @Deprecated - public Channel getSftpChannel() throws JSchException { - return sock.openChannel("sftp"); //$NON-NLS-1$ - } - - /** - * {@inheritDoc} - * - * @since 5.2 - */ - @Override - public FtpChannel getFtpChannel() { - return new JschFtpChannel(); - } - - /** - * Implementation of Process for running a single command using Jsch. - *

- * Uses the Jsch session to do actual command execution and manage the - * execution. - */ - private class JschProcess extends Process { - private ChannelExec channel; - - final int timeout; - - private InputStream inputStream; - - private OutputStream outputStream; - - private InputStream errStream; - - /** - * Opens a channel on the session ("sock") for executing the given - * command, opens streams, and starts command execution. - * - * @param commandName - * the command to execute - * @param environment - * environment variables to pass on - * @param tms - * the timeout value, in seconds, for the command. - * @throws TransportException - * on problems opening a channel or connecting to the remote - * host - * @throws IOException - * on problems opening streams - */ - JschProcess(String commandName, Map environment, - int tms) throws TransportException, IOException { - timeout = tms; - try { - channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$ - if (environment != null) { - for (Map.Entry envVar : environment - .entrySet()) { - channel.setEnv(envVar.getKey(), envVar.getValue()); - } - } - channel.setCommand(commandName); - setupStreams(); - channel.connect(timeout > 0 ? timeout * 1000 : 0); - if (!channel.isConnected()) { - closeOutputStream(); - throw new TransportException(uri, - JSchText.get().connectionFailed); - } - } catch (JSchException e) { - closeOutputStream(); - throw new TransportException(uri, e.getMessage(), e); - } - } - - private void closeOutputStream() { - if (outputStream != null) { - try { - outputStream.close(); - } catch (IOException ioe) { - // ignore - } - } - } - - private void setupStreams() throws IOException { - inputStream = channel.getInputStream(); - - // JSch won't let us interrupt writes when we use our InterruptTimer - // to break out of a long-running write operation. To work around - // that we spawn a background thread to shuttle data through a pipe, - // as we can issue an interrupted write out of that. Its slower, so - // we only use this route if there is a timeout. - OutputStream out = channel.getOutputStream(); - if (timeout <= 0) { - outputStream = out; - } else { - IsolatedOutputStream i = new IsolatedOutputStream(out); - outputStream = new BufferedOutputStream(i, 16 * 1024); - } - - errStream = channel.getErrStream(); - } - - @Override - public InputStream getInputStream() { - return inputStream; - } - - @Override - public OutputStream getOutputStream() { - return outputStream; - } - - @Override - public InputStream getErrorStream() { - return errStream; - } - - @Override - public int exitValue() { - if (isRunning()) - throw new IllegalThreadStateException(); - return channel.getExitStatus(); - } - - private boolean isRunning() { - return channel.getExitStatus() < 0 && channel.isConnected(); - } - - @Override - public void destroy() { - if (channel.isConnected()) - channel.disconnect(); - closeOutputStream(); - } - - @Override - public int waitFor() throws InterruptedException { - while (isRunning()) - Thread.sleep(100); - return exitValue(); - } - } - - private class JschFtpChannel implements FtpChannel { - - private ChannelSftp ftp; - - @Override - public void connect(int timeout, TimeUnit unit) throws IOException { - try { - ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$ - ftp.connect((int) unit.toMillis(timeout)); - } catch (JSchException e) { - ftp = null; - throw new IOException(e.getLocalizedMessage(), e); - } - } - - @Override - public void disconnect() { - ftp.disconnect(); - ftp = null; - } - - private T map(Callable op) throws IOException { - try { - return op.call(); - } catch (Exception e) { - if (e instanceof SftpException) { - throw new FtpChannel.FtpException(e.getLocalizedMessage(), - ((SftpException) e).id, e); - } - throw new IOException(e.getLocalizedMessage(), e); - } - } - - @Override - public boolean isConnected() { - return ftp != null && sock.isConnected(); - } - - @Override - public void cd(String path) throws IOException { - map(() -> { - ftp.cd(path); - return null; - }); - } - - @Override - public String pwd() throws IOException { - return map(() -> ftp.pwd()); - } - - @Override - public Collection ls(String path) throws IOException { - return map(() -> { - List result = new ArrayList<>(); - for (Object e : ftp.ls(path)) { - ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e; - result.add(new DirEntry() { - - @Override - public String getFilename() { - return entry.getFilename(); - } - - @Override - public long getModifiedTime() { - return entry.getAttrs().getMTime(); - } - - @Override - public boolean isDirectory() { - return entry.getAttrs().isDir(); - } - }); - } - return result; - }); - } - - @Override - public void rmdir(String path) throws IOException { - map(() -> { - ftp.rm(path); - return null; - }); - } - - @Override - public void mkdir(String path) throws IOException { - map(() -> { - ftp.mkdir(path); - return null; - }); - } - - @Override - public InputStream get(String path) throws IOException { - return map(() -> ftp.get(path)); - } - - @Override - public OutputStream put(String path) throws IOException { - return map(() -> ftp.put(path)); - } - - @Override - public void rm(String path) throws IOException { - map(() -> { - ftp.rm(path); - return null; - }); - } - - @Override - public void rename(String from, String to) throws IOException { - map(() -> { - // Plain FTP rename will fail if "to" exists. Jsch knows about - // the FTP extension "posix-rename@openssh.com", which will - // remove "to" first if it exists. - if (hasPosixRename()) { - ftp.rename(from, to); - } else if (!to.equals(from)) { - // Try to remove "to" first. With git, we typically get this - // when a lock file is moved over the file locked. Note that - // the check for to being equal to from may still fail in - // the general case, but for use with JGit's TransportSftp - // it should be good enough. - delete(to); - ftp.rename(from, to); - } - return null; - }); - } - - /** - * Determine whether the server has the posix-rename extension. - * - * @return {@code true} if it is supported, {@code false} otherwise - * @see OpenSSH - * deviations and extensions to the published SSH protocol - * @see stdio.h: - * rename() - */ - private boolean hasPosixRename() { - return "1".equals(ftp.getExtension("posix-rename@openssh.com")); //$NON-NLS-1$//$NON-NLS-2$ - } - } -} diff --git a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/OpenSshConfig.java b/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/OpenSshConfig.java deleted file mode 100644 index fce2d87e0..000000000 --- a/jgit-ssh-jsch/src/main/java/org/openrewrite/jgit/transport/OpenSshConfig.java +++ /dev/null @@ -1,346 +0,0 @@ -/* - * Copyright (C) 2008, 2018, Google Inc. and others - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Distribution License v. 1.0 which is available at - * https://www.eclipse.org/org/documents/edl-v10.php. - * - * SPDX-License-Identifier: BSD-3-Clause - */ - -//TODO(ms): move to org.openrewrite.jgit.ssh.jsch in 6.0 -package org.openrewrite.jgit.transport; - -import static org.openrewrite.jgit.internal.transport.ssh.OpenSshConfigFile.positive; - -import java.io.File; -import java.util.List; -import java.util.Map; -import java.util.TreeMap; - -import org.openrewrite.jgit.internal.transport.ssh.OpenSshConfigFile; -import org.openrewrite.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry; -import org.openrewrite.jgit.util.FS; - -import com.jcraft.jsch.ConfigRepository; - -/** - * Fairly complete configuration parser for the OpenSSH ~/.ssh/config file. - *

- * JSch does have its own config file parser - * {@link com.jcraft.jsch.OpenSSHConfig} since version 0.1.50, but it has a - * number of problems: - *

    - *
  • it splits lines of the format "keyword = value" wrongly: you'd end up - * with the value "= value". - *
  • its "Host" keyword is not case insensitive. - *
  • it doesn't handle quoted values. - *
  • JSch's OpenSSHConfig doesn't monitor for config file changes. - *
- *

- * This parser makes the critical options available to - * {@link org.openrewrite.jgit.transport.SshSessionFactory} via - * {@link org.openrewrite.jgit.transport.OpenSshConfig.Host} objects returned - * by {@link #lookup(String)}, and implements a fully conforming - * {@link com.jcraft.jsch.ConfigRepository} providing - * {@link com.jcraft.jsch.ConfigRepository.Config}s via - * {@link #getConfig(String)}. - *

- * - * @see OpenSshConfigFile - */ -public class OpenSshConfig implements ConfigRepository { - - /** - * Obtain the user's configuration data. - *

- * The configuration file is always returned to the caller, even if no file - * exists in the user's home directory at the time the call was made. Lookup - * requests are cached and are automatically updated if the user modifies - * the configuration file since the last time it was cached. - * - * @param fs - * the file system abstraction which will be necessary to - * perform certain file system operations. - * @return a caching reader of the user's configuration file. - */ - public static OpenSshConfig get(FS fs) { - File home = fs.userHome(); - if (home == null) - home = new File(".").getAbsoluteFile(); //$NON-NLS-1$ - - final File config = new File(new File(home, SshConstants.SSH_DIR), - SshConstants.CONFIG); - return new OpenSshConfig(home, config); - } - - /** The base file. */ - private OpenSshConfigFile configFile; - - OpenSshConfig(File h, File cfg) { - configFile = new OpenSshConfigFile(h, cfg, - SshSessionFactory.getLocalUserName()); - } - - /** - * Locate the configuration for a specific host request. - * - * @param hostName - * the name the user has supplied to the SSH tool. This may be a - * real host name, or it may just be a "Host" block in the - * configuration file. - * @return r configuration for the requested name. Never null. - */ - public Host lookup(String hostName) { - HostEntry entry = configFile.lookup(hostName, -1, null); - return new Host(entry, hostName, configFile.getLocalUserName()); - } - - /** - * Configuration of one "Host" block in the configuration file. - *

- * If returned from {@link OpenSshConfig#lookup(String)} some or all of the - * properties may not be populated. The properties which are not populated - * should be defaulted by the caller. - *

- * When returned from {@link OpenSshConfig#lookup(String)} any wildcard - * entries which appear later in the configuration file will have been - * already merged into this block. - */ - public static class Host { - String hostName; - - int port; - - File identityFile; - - String user; - - String preferredAuthentications; - - Boolean batchMode; - - String strictHostKeyChecking; - - int connectionAttempts; - - private HostEntry entry; - - private Config config; - - // See com.jcraft.jsch.OpenSSHConfig. Translates some command-line keys - // to ssh-config keys. - private static final Map KEY_MAP = new TreeMap<>( - String.CASE_INSENSITIVE_ORDER); - - static { - KEY_MAP.put("kex", SshConstants.KEX_ALGORITHMS); //$NON-NLS-1$ - KEY_MAP.put("server_host_key", SshConstants.HOST_KEY_ALGORITHMS); //$NON-NLS-1$ - KEY_MAP.put("cipher.c2s", SshConstants.CIPHERS); //$NON-NLS-1$ - KEY_MAP.put("cipher.s2c", SshConstants.CIPHERS); //$NON-NLS-1$ - KEY_MAP.put("mac.c2s", SshConstants.MACS); //$NON-NLS-1$ - KEY_MAP.put("mac.s2c", SshConstants.MACS); //$NON-NLS-1$ - KEY_MAP.put("compression.s2c", SshConstants.COMPRESSION); //$NON-NLS-1$ - KEY_MAP.put("compression.c2s", SshConstants.COMPRESSION); //$NON-NLS-1$ - KEY_MAP.put("compression_level", "CompressionLevel"); //$NON-NLS-1$ //$NON-NLS-2$ - KEY_MAP.put("MaxAuthTries", //$NON-NLS-1$ - SshConstants.NUMBER_OF_PASSWORD_PROMPTS); - } - - private static String mapKey(String key) { - String k = KEY_MAP.get(key); - return k != null ? k : key; - } - - /** - * Creates a new uninitialized {@link Host}. - */ - public Host() { - // For API backwards compatibility with pre-4.9 JGit - } - - Host(HostEntry entry, String hostName, String localUserName) { - this.entry = entry; - complete(hostName, localUserName); - } - - /** - * @return the value StrictHostKeyChecking property, the valid values - * are "yes" (unknown hosts are not accepted), "no" (unknown - * hosts are always accepted), and "ask" (user should be asked - * before accepting the host) - */ - public String getStrictHostKeyChecking() { - return strictHostKeyChecking; - } - - /** - * @return the real IP address or host name to connect to; never null. - */ - public String getHostName() { - return hostName; - } - - /** - * @return the real port number to connect to; never 0. - */ - public int getPort() { - return port; - } - - /** - * @return path of the private key file to use for authentication; null - * if the caller should use default authentication strategies. - */ - public File getIdentityFile() { - return identityFile; - } - - /** - * @return the real user name to connect as; never null. - */ - public String getUser() { - return user; - } - - /** - * @return the preferred authentication methods, separated by commas if - * more than one authentication method is preferred. - */ - public String getPreferredAuthentications() { - return preferredAuthentications; - } - - /** - * @return true if batch (non-interactive) mode is preferred for this - * host connection. - */ - public boolean isBatchMode() { - return batchMode != null && batchMode.booleanValue(); - } - - /** - * @return the number of tries (one per second) to connect before - * exiting. The argument must be an integer. This may be useful - * in scripts if the connection sometimes fails. The default is - * 1. - * @since 3.4 - */ - public int getConnectionAttempts() { - return connectionAttempts; - } - - - private void complete(String initialHostName, String localUserName) { - // Try to set values from the options. - hostName = entry.getValue(SshConstants.HOST_NAME); - user = entry.getValue(SshConstants.USER); - port = positive(entry.getValue(SshConstants.PORT)); - connectionAttempts = positive( - entry.getValue(SshConstants.CONNECTION_ATTEMPTS)); - strictHostKeyChecking = entry - .getValue(SshConstants.STRICT_HOST_KEY_CHECKING); - batchMode = Boolean.valueOf(OpenSshConfigFile - .flag(entry.getValue(SshConstants.BATCH_MODE))); - preferredAuthentications = entry - .getValue(SshConstants.PREFERRED_AUTHENTICATIONS); - // Fill in defaults if still not set - if (hostName == null || hostName.isEmpty()) { - hostName = initialHostName; - } - if (user == null || user.isEmpty()) { - user = localUserName; - } - if (port <= 0) { - port = SshConstants.SSH_DEFAULT_PORT; - } - if (connectionAttempts <= 0) { - connectionAttempts = 1; - } - List identityFiles = entry - .getValues(SshConstants.IDENTITY_FILE); - if (identityFiles != null && !identityFiles.isEmpty()) { - identityFile = new File(identityFiles.get(0)); - } - } - - Config getConfig() { - if (config == null) { - config = new Config() { - - @Override - public String getHostname() { - return Host.this.getHostName(); - } - - @Override - public String getUser() { - return Host.this.getUser(); - } - - @Override - public int getPort() { - return Host.this.getPort(); - } - - @Override - public String getValue(String key) { - // See com.jcraft.jsch.OpenSSHConfig.MyConfig.getValue() - // for this special case. - if (key.equals("compression.s2c") //$NON-NLS-1$ - || key.equals("compression.c2s")) { //$NON-NLS-1$ - if (!OpenSshConfigFile.flag( - Host.this.entry.getValue(mapKey(key)))) { - return "none,zlib@openssh.com,zlib"; //$NON-NLS-1$ - } - return "zlib@openssh.com,zlib,none"; //$NON-NLS-1$ - } - return Host.this.entry.getValue(mapKey(key)); - } - - @Override - public String[] getValues(String key) { - List values = Host.this.entry - .getValues(mapKey(key)); - if (values == null) { - return new String[0]; - } - return values.toArray(new String[0]); - } - }; - } - return config; - } - - @Override - @SuppressWarnings("nls") - public String toString() { - return "Host [hostName=" + hostName + ", port=" + port - + ", identityFile=" + identityFile + ", user=" + user - + ", preferredAuthentications=" + preferredAuthentications - + ", batchMode=" + batchMode + ", strictHostKeyChecking=" - + strictHostKeyChecking + ", connectionAttempts=" - + connectionAttempts + ", entry=" + entry + "]"; - } - } - - /** - * {@inheritDoc} - *

- * Retrieves the full {@link com.jcraft.jsch.ConfigRepository.Config Config} - * for the given host name. Should be called only by Jsch and tests. - * - * @since 4.9 - */ - @Override - public Config getConfig(String hostName) { - Host host = lookup(hostName); - return host.getConfig(); - } - - /** {@inheritDoc} */ - @Override - public String toString() { - return "OpenSshConfig [configFile=" + configFile + ']'; //$NON-NLS-1$ - } -} diff --git a/settings.gradle.kts b/settings.gradle.kts index 432b13350..2d54e2d6f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,4 +9,3 @@ rootProject.name = "openrewrite-jgit" include("jgit") include("jgit-gpg-bc") -include("jgit-ssh-jsch")