Skip to main content
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to 'org.eclipse.jgit.ssh.apache/src/org/eclipse')
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java238
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java60
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java33
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java93
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java19
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java33
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java369
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java7
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java21
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java26
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java26
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java261
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java40
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java19
14 files changed, 1103 insertions, 142 deletions
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java
new file mode 100644
index 0000000000..add79b35c9
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/AuthenticationLogger.java
@@ -0,0 +1,238 @@
+/*
+ * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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.eclipse.jgit.internal.transport.sshd;
+
+import static org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider.getKeyId;
+
+import java.security.KeyPair;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.apache.sshd.client.auth.password.PasswordAuthenticationReporter;
+import org.apache.sshd.client.auth.password.UserAuthPassword;
+import org.apache.sshd.client.auth.pubkey.PublicKeyAuthenticationReporter;
+import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.config.keys.KeyUtils;
+
+/**
+ * Provides a log of authentication attempts for a {@link ClientSession}.
+ */
+public class AuthenticationLogger {
+
+ private final List<String> messages = new ArrayList<>();
+
+ // We're interested in this log only in the failure case, so we don't need
+ // to log authentication success.
+
+ private final PublicKeyAuthenticationReporter pubkeyLogger = new PublicKeyAuthenticationReporter() {
+
+ private boolean hasAttempts;
+
+ @Override
+ public void signalAuthenticationAttempt(ClientSession session,
+ String service, KeyPair identity, String signature)
+ throws Exception {
+ hasAttempts = true;
+ String message;
+ if (identity.getPrivate() == null) {
+ // SSH agent key
+ message = MessageFormat.format(
+ SshdText.get().authPubkeyAttemptAgent,
+ UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
+ getKeyId(session, identity), signature);
+ } else {
+ message = MessageFormat.format(
+ SshdText.get().authPubkeyAttempt,
+ UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
+ getKeyId(session, identity), signature);
+ }
+ messages.add(message);
+ }
+
+ @Override
+ public void signalAuthenticationExhausted(ClientSession session,
+ String service) throws Exception {
+ String message;
+ if (hasAttempts) {
+ message = MessageFormat.format(
+ SshdText.get().authPubkeyExhausted,
+ UserAuthPublicKey.NAME);
+ } else {
+ message = MessageFormat.format(SshdText.get().authPubkeyNoKeys,
+ UserAuthPublicKey.NAME);
+ }
+ messages.add(message);
+ hasAttempts = false;
+ }
+
+ @Override
+ public void signalAuthenticationFailure(ClientSession session,
+ String service, KeyPair identity, boolean partial,
+ List<String> serverMethods) throws Exception {
+ String message;
+ if (partial) {
+ message = MessageFormat.format(
+ SshdText.get().authPubkeyPartialSuccess,
+ UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
+ getKeyId(session, identity), serverMethods);
+ } else {
+ message = MessageFormat.format(
+ SshdText.get().authPubkeyFailure,
+ UserAuthPublicKey.NAME, KeyUtils.getKeyType(identity),
+ getKeyId(session, identity));
+ }
+ messages.add(message);
+ }
+ };
+
+ private final PasswordAuthenticationReporter passwordLogger = new PasswordAuthenticationReporter() {
+
+ private int attempts;
+
+ @Override
+ public void signalAuthenticationAttempt(ClientSession session,
+ String service, String oldPassword, boolean modified,
+ String newPassword) throws Exception {
+ attempts++;
+ String message;
+ if (modified) {
+ message = MessageFormat.format(
+ SshdText.get().authPasswordChangeAttempt,
+ UserAuthPassword.NAME, Integer.valueOf(attempts));
+ } else {
+ message = MessageFormat.format(
+ SshdText.get().authPasswordAttempt,
+ UserAuthPassword.NAME, Integer.valueOf(attempts));
+ }
+ messages.add(message);
+ }
+
+ @Override
+ public void signalAuthenticationExhausted(ClientSession session,
+ String service) throws Exception {
+ String message;
+ if (attempts > 0) {
+ message = MessageFormat.format(
+ SshdText.get().authPasswordExhausted,
+ UserAuthPassword.NAME);
+ } else {
+ message = MessageFormat.format(
+ SshdText.get().authPasswordNotTried,
+ UserAuthPassword.NAME);
+ }
+ messages.add(message);
+ attempts = 0;
+ }
+
+ @Override
+ public void signalAuthenticationFailure(ClientSession session,
+ String service, String password, boolean partial,
+ List<String> serverMethods) throws Exception {
+ String message;
+ if (partial) {
+ message = MessageFormat.format(
+ SshdText.get().authPasswordPartialSuccess,
+ UserAuthPassword.NAME, serverMethods);
+ } else {
+ message = MessageFormat.format(
+ SshdText.get().authPasswordFailure,
+ UserAuthPassword.NAME);
+ }
+ messages.add(message);
+ }
+ };
+
+ private final GssApiWithMicAuthenticationReporter gssLogger = new GssApiWithMicAuthenticationReporter() {
+
+ private boolean hasAttempts;
+
+ @Override
+ public void signalAuthenticationAttempt(ClientSession session,
+ String service, String mechanism) {
+ hasAttempts = true;
+ String message = MessageFormat.format(
+ SshdText.get().authGssApiAttempt,
+ GssApiWithMicAuthFactory.NAME, mechanism);
+ messages.add(message);
+ }
+
+ @Override
+ public void signalAuthenticationExhausted(ClientSession session,
+ String service) {
+ String message;
+ if (hasAttempts) {
+ message = MessageFormat.format(
+ SshdText.get().authGssApiExhausted,
+ GssApiWithMicAuthFactory.NAME);
+ } else {
+ message = MessageFormat.format(
+ SshdText.get().authGssApiNotTried,
+ GssApiWithMicAuthFactory.NAME);
+ }
+ messages.add(message);
+ hasAttempts = false;
+ }
+
+ @Override
+ public void signalAuthenticationFailure(ClientSession session,
+ String service, String mechanism, boolean partial,
+ List<String> serverMethods) {
+ String message;
+ if (partial) {
+ message = MessageFormat.format(
+ SshdText.get().authGssApiPartialSuccess,
+ GssApiWithMicAuthFactory.NAME, mechanism,
+ serverMethods);
+ } else {
+ message = MessageFormat.format(
+ SshdText.get().authGssApiFailure,
+ GssApiWithMicAuthFactory.NAME, mechanism);
+ }
+ messages.add(message);
+ }
+ };
+
+ /**
+ * Creates a new {@link AuthenticationLogger} and configures the
+ * {@link ClientSession} to report authentication attempts through this
+ * instance.
+ *
+ * @param session
+ * to configure
+ */
+ public AuthenticationLogger(ClientSession session) {
+ session.setPublicKeyAuthenticationReporter(pubkeyLogger);
+ session.setPasswordAuthenticationReporter(passwordLogger);
+ session.setAttribute(
+ GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER,
+ gssLogger);
+ // TODO: keyboard-interactive? sshd 2.8.0 has no callback
+ // interface for it.
+ }
+
+ /**
+ * Retrieves the log messages for the authentication attempts.
+ *
+ * @return the messages as an unmodifiable list
+ */
+ public List<String> getLog() {
+ return Collections.unmodifiableList(messages);
+ }
+
+ /**
+ * Drops all previously recorded log messages.
+ */
+ public void clear() {
+ messages.clear();
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java
index 79b3637caa..cbd6a64140 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -11,6 +11,7 @@ package org.eclipse.jgit.internal.transport.sshd;
import static java.text.MessageFormat.format;
+import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
@@ -19,18 +20,24 @@ import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.PrivateKey;
+import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.CancellationException;
import javax.security.auth.DestroyFailedException;
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
+import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.io.resource.IoResource;
@@ -43,6 +50,14 @@ import org.eclipse.jgit.transport.sshd.KeyCache;
public class CachingKeyPairProvider extends FileKeyPairProvider
implements Iterable<KeyPair> {
+ /**
+ * An attribute set on the {@link SessionContext} recording loaded keys by
+ * fingerprint. This enables us to provide nicer output by showing key
+ * paths, if possible. Users can identify key identities used easier by
+ * filename than by fingerprint.
+ */
+ public static final AttributeKey<Map<String, Path>> KEY_PATHS_BY_FINGERPRINT = new AttributeKey<>();
+
private final KeyCache cache;
/**
@@ -78,6 +93,33 @@ public class CachingKeyPairProvider extends FileKeyPairProvider
return () -> iterator(session);
}
+ static String getKeyId(ClientSession session, KeyPair identity) {
+ String fingerprint = KeyUtils.getFingerPrint(identity.getPublic());
+ Map<String, Path> registered = session
+ .getAttribute(KEY_PATHS_BY_FINGERPRINT);
+ if (registered != null) {
+ Path path = registered.get(fingerprint);
+ if (path != null) {
+ Path home = session
+ .resolveAttribute(JGitSshClient.HOME_DIRECTORY);
+ if (home != null && path.startsWith(home)) {
+ try {
+ path = home.relativize(path);
+ String pathString = path.toString();
+ if (!pathString.isEmpty()) {
+ return "~" + File.separator + pathString; //$NON-NLS-1$
+ }
+ } catch (IllegalArgumentException e) {
+ // Cannot be relativized. Ignore, and work with the
+ // original path
+ }
+ }
+ return path.toString();
+ }
+ }
+ return fingerprint;
+ }
+
private KeyPair loadKey(SessionContext session, Path path)
throws IOException, GeneralSecurityException {
if (!Files.exists(path)) {
@@ -123,13 +165,23 @@ public class CachingKeyPairProvider extends FileKeyPairProvider
SshdText.get().identityFileUnsupportedFormat, path));
}
KeyPair result = keys.next();
+ PublicKey pk = result.getPublic();
+ if (pk != null) {
+ Map<String, Path> registered = session
+ .getAttribute(KEY_PATHS_BY_FINGERPRINT);
+ if (registered == null) {
+ registered = new HashMap<>();
+ session.setAttribute(KEY_PATHS_BY_FINGERPRINT, registered);
+ }
+ registered.put(KeyUtils.getFingerPrint(pk), path);
+ }
if (keys.hasNext()) {
log.warn(format(SshdText.get().identityFileMultipleKeys, path));
keys.forEachRemaining(k -> {
- PrivateKey pk = k.getPrivate();
- if (pk != null) {
+ PrivateKey priv = k.getPrivate();
+ if (priv != null) {
try {
- pk.destroy();
+ priv.destroy();
} catch (DestroyFailedException e) {
// Ignore
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java
index c3cac0c1df..df01db316b 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthentication.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -18,6 +18,7 @@ import java.net.SocketAddress;
import java.net.UnknownHostException;
import java.util.Collection;
import java.util.Iterator;
+import java.util.List;
import org.apache.sshd.client.auth.AbstractUserAuth;
import org.apache.sshd.client.session.ClientSession;
@@ -71,7 +72,10 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth {
if (context != null) {
close(false);
}
+ GssApiWithMicAuthenticationReporter reporter = session.getAttribute(
+ GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER);
if (!nextMechanism.hasNext()) {
+ reporter.signalAuthenticationExhausted(session, service);
return false;
}
state = ProtocolState.STARTED;
@@ -79,6 +83,7 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth {
// RFC 4462 states that SPNEGO must not be used with ssh
while (GssApiMechanisms.SPNEGO.equals(currentMechanism)) {
if (!nextMechanism.hasNext()) {
+ reporter.signalAuthenticationExhausted(session, service);
return false;
}
currentMechanism = nextMechanism.next();
@@ -102,6 +107,10 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth {
state = ProtocolState.FAILED;
return false;
}
+ if (reporter != null) {
+ reporter.signalAuthenticationAttempt(session, service,
+ currentMechanism.toString());
+ }
Buffer buffer = session
.createBuffer(SshConstants.SSH_MSG_USERAUTH_REQUEST);
buffer.putString(session.getUsername());
@@ -246,4 +255,26 @@ public class GssApiWithMicAuthentication extends AbstractUserAuth {
return false;
}
+ @Override
+ public void signalAuthMethodSuccess(ClientSession session, String service,
+ Buffer buffer) throws Exception {
+ GssApiWithMicAuthenticationReporter reporter = session.getAttribute(
+ GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER);
+ if (reporter != null) {
+ reporter.signalAuthenticationSuccess(session, service,
+ currentMechanism.toString());
+ }
+ }
+
+ @Override
+ public void signalAuthMethodFailure(ClientSession session, String service,
+ boolean partial, List<String> serverMethods, Buffer buffer)
+ throws Exception {
+ GssApiWithMicAuthenticationReporter reporter = session.getAttribute(
+ GssApiWithMicAuthenticationReporter.GSS_AUTHENTICATION_REPORTER);
+ if (reporter != null) {
+ reporter.signalAuthenticationFailure(session, service,
+ currentMechanism.toString(), partial, serverMethods);
+ }
+ }
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java
new file mode 100644
index 0000000000..201a131650
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/GssApiWithMicAuthenticationReporter.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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.eclipse.jgit.internal.transport.sshd;
+
+import java.util.List;
+
+import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.AttributeRepository.AttributeKey;
+
+/**
+ * Callback interface for recording authentication state in
+ * {@link GssApiWithMicAuthentication}.
+ */
+public interface GssApiWithMicAuthenticationReporter {
+
+ /**
+ * An {@link AttributeKey} for a {@link ClientSession} holding the
+ * {@link GssApiWithMicAuthenticationReporter}.
+ */
+ static final AttributeKey<GssApiWithMicAuthenticationReporter> GSS_AUTHENTICATION_REPORTER = new AttributeKey<>();
+
+ /**
+ * Called when a new authentication attempt is made.
+ *
+ * @param session
+ * the {@link ClientSession}
+ * @param service
+ * the name of the requesting SSH service name
+ * @param mechanism
+ * the OID of the mechanism used
+ */
+ default void signalAuthenticationAttempt(ClientSession session,
+ String service, String mechanism) {
+ // nothing
+ }
+
+ /**
+ * Called when there are no more mechanisms to try.
+ *
+ * @param session
+ * the {@link ClientSession}
+ * @param service
+ * the name of the requesting SSH service name
+ */
+ default void signalAuthenticationExhausted(ClientSession session,
+ String service) {
+ // nothing
+ }
+
+ /**
+ * Called when authentication was succeessful.
+ *
+ * @param session
+ * the {@link ClientSession}
+ * @param service
+ * the name of the requesting SSH service name
+ * @param mechanism
+ * the OID of the mechanism used
+ */
+ default void signalAuthenticationSuccess(ClientSession session,
+ String service, String mechanism) {
+ // nothing
+ }
+
+ /**
+ * Called when the authentication was not successful.
+ *
+ * @param session
+ * the {@link ClientSession}
+ * @param service
+ * the name of the requesting SSH service name
+ * @param mechanism
+ * the OID of the mechanism used
+ * @param partial
+ * {@code true} if authentication was partially successful,
+ * meaning one continues with additional authentication methods
+ * given by {@code serverMethods}
+ * @param serverMethods
+ * the {@link List} of authentication methods that can continue
+ */
+ default void signalAuthenticationFailure(ClientSession session,
+ String service, String mechanism, boolean partial,
+ List<String> serverMethods) {
+ // nothing
+ }
+}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
index e2dbb4c466..5100bc9e54 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitClientSession.java
@@ -42,7 +42,6 @@ import org.apache.sshd.common.io.IoSession;
import org.apache.sshd.common.io.IoWriteFuture;
import org.apache.sshd.common.kex.BuiltinDHFactories;
import org.apache.sshd.common.kex.DHFactory;
-import org.apache.sshd.common.kex.KexProposalOption;
import org.apache.sshd.common.kex.KeyExchangeFactory;
import org.apache.sshd.common.kex.extension.KexExtensionHandler;
import org.apache.sshd.common.kex.extension.KexExtensionHandler.AvailabilityPhase;
@@ -199,24 +198,6 @@ public class JGitClientSession extends ClientSessionImpl {
}
}
- @Override
- protected Map<KexProposalOption, String> setNegotiationResult(
- Map<KexProposalOption, String> guess) {
- Map<KexProposalOption, String> result = super.setNegotiationResult(
- guess);
- // This should be doable with a SessionListener, too, but I don't see
- // how to add a listener in time to catch the negotiation end for sure
- // given that the super-constructor already starts KEX.
- //
- // TODO: This override can be removed once we use sshd 2.8.0.
- if (log.isDebugEnabled()) {
- result.forEach((option, value) -> log.debug(
- "setNegotiationResult({}) Kex: {} = {}", this, //$NON-NLS-1$
- option.getDescription(), value));
- }
- return result;
- }
-
Set<String> getAllAvailableSignatureAlgorithms() {
Set<String> allAvailable = new HashSet<>();
BuiltinSignatures.VALUES.forEach(s -> allAvailable.add(s.getName()));
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java
index ff8caaacc0..33c3c608f6 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPasswordAuthentication.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -11,13 +11,11 @@ package org.eclipse.jgit.internal.transport.sshd;
import static org.apache.sshd.core.CoreModuleProperties.PASSWORD_PROMPTS;
-import org.apache.sshd.client.auth.keyboard.UserInteraction;
import org.apache.sshd.client.auth.password.UserAuthPassword;
import org.apache.sshd.client.session.ClientSession;
/**
- * A password authentication handler that uses the {@link JGitUserInteraction}
- * to ask the user for the password. It also respects the
+ * A password authentication handler that respects the
* {@code NumberOfPasswordPrompts} ssh config.
*/
public class JGitPasswordAuthentication extends UserAuthPassword {
@@ -35,30 +33,11 @@ public class JGitPasswordAuthentication extends UserAuthPassword {
}
@Override
- protected boolean sendAuthDataRequest(ClientSession session, String service)
- throws Exception {
+ protected String resolveAttemptedPassword(ClientSession session,
+ String service) throws Exception {
if (++attempts > maxAttempts) {
- return false;
+ return null;
}
- UserInteraction interaction = session.getUserInteraction();
- if (!interaction.isInteractionAllowed(session)) {
- return false;
- }
- String password = getPassword(session, interaction);
- if (password == null) {
- throw new AuthenticationCanceledException();
- }
- // sendPassword takes a buffer as first argument, but actually doesn't
- // use it and creates its own buffer...
- sendPassword(null, session, password, password);
- return true;
- }
-
- private String getPassword(ClientSession session,
- UserInteraction interaction) {
- String[] results = interaction.interactive(session, null, null, "", //$NON-NLS-1$
- new String[] { SshdText.get().passwordPrompt },
- new boolean[] { false });
- return (results == null || results.length == 0) ? null : results[0];
+ return super.resolveAttemptedPassword(session, service);
}
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java
index c082a9a963..e1036c6283 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitPublicKeyAuthentication.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -12,25 +12,68 @@ package org.eclipse.jgit.internal.transport.sshd;
import static java.text.MessageFormat.format;
import static org.eclipse.jgit.transport.SshConstants.PUBKEY_ACCEPTED_ALGORITHMS;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.apache.sshd.agent.SshAgent;
+import org.apache.sshd.agent.SshAgentFactory;
+import org.apache.sshd.agent.SshAgentKeyConstraint;
import org.apache.sshd.client.auth.pubkey.KeyAgentIdentity;
import org.apache.sshd.client.auth.pubkey.PublicKeyIdentity;
import org.apache.sshd.client.auth.pubkey.UserAuthPublicKey;
+import org.apache.sshd.client.auth.pubkey.UserAuthPublicKeyIterator;
import org.apache.sshd.client.config.hosts.HostConfigEntry;
import org.apache.sshd.client.session.ClientSession;
+import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.NamedFactory;
+import org.apache.sshd.common.config.keys.AuthorizedKeyEntry;
+import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.config.keys.PublicKeyEntryResolver;
+import org.apache.sshd.common.config.keys.u2f.SecurityKeyPublicKey;
import org.apache.sshd.common.signature.Signature;
+import org.apache.sshd.common.signature.SignatureFactoriesManager;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.transport.CredentialItem;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.SshConstants;
+import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.StringUtils;
/**
* Custom {@link UserAuthPublicKey} implementation for handling SSH config
- * PubkeyAcceptedAlgorithms.
+ * PubkeyAcceptedAlgorithms and interaction with the SSH agent.
*/
public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
+ private SshAgent agent;
+
+ private HostConfigEntry hostConfig;
+
+ private boolean addKeysToAgent;
+
+ private boolean askBeforeAdding;
+
+ private String skProvider;
+
+ private SshAgentKeyConstraint[] constraints;
+
JGitPublicKeyAuthentication(List<NamedFactory<Signature>> factories) {
super(factories);
}
@@ -43,7 +86,7 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
+ rawSession.getClass().getCanonicalName());
}
JGitClientSession session = (JGitClientSession) rawSession;
- HostConfigEntry hostConfig = session.getHostConfigEntry();
+ hostConfig = session.getHostConfigEntry();
// Set signature algorithms for public key authentication
String pubkeyAlgos = hostConfig.getProperty(PUBKEY_ACCEPTED_ALGORITHMS);
if (!StringUtils.isEmptyOrNull(pubkeyAlgos)) {
@@ -56,54 +99,304 @@ public class JGitPublicKeyAuthentication extends UserAuthPublicKey {
log.debug(PUBKEY_ACCEPTED_ALGORITHMS + ' ' + signatures);
}
setSignatureFactoriesNames(signatures);
- } else {
- log.warn(format(SshdText.get().configNoKnownAlgorithms,
- PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos));
+ super.init(session, service);
+ return;
}
+ log.warn(format(SshdText.get().configNoKnownAlgorithms,
+ PUBKEY_ACCEPTED_ALGORITHMS, pubkeyAlgos));
+ }
+ // TODO: remove this once we're on an sshd version that has SSHD-1272
+ // fixed
+ List<NamedFactory<Signature>> localFactories = getSignatureFactories();
+ if (localFactories == null || localFactories.isEmpty()) {
+ setSignatureFactoriesNames(session.getSignatureFactoriesNames());
}
- // If we don't set signature factories here, the default ones from the
- // session will be used.
super.init(session, service);
- // In sshd 2.7.0, we end up now with a key iterator that uses keys
- // provided by an ssh-agent even if IdentitiesOnly is true. So if
- // needed, filter out any KeyAgentIdentity.
- if (hostConfig.isIdentitiesOnly()) {
- Iterator<PublicKeyIdentity> original = keys;
- // The original iterator will already have gotten the identities
- // from the agent. Unfortunately there's nothing we can do about
- // that; it'll have to be fixed upstream. (As will, ultimately,
- // respecting isIdentitiesOnly().) At least we can simply not
- // use the keys the agent provided.
- //
- // See https://issues.apache.org/jira/browse/SSHD-1218
- keys = new Iterator<>() {
-
- private PublicKeyIdentity value;
+ }
+
+ @Override
+ protected Iterator<PublicKeyIdentity> createPublicKeyIterator(
+ ClientSession session, SignatureFactoriesManager manager)
+ throws Exception {
+ agent = getAgent(session);
+ if (agent != null) {
+ parseAddKeys(hostConfig);
+ if (addKeysToAgent) {
+ skProvider = hostConfig.getProperty(SshConstants.SECURITY_KEY_PROVIDER);
+ }
+ }
+ return new KeyIterator(session, manager);
+ }
+
+ @Override
+ protected PublicKeyIdentity resolveAttemptedPublicKeyIdentity(
+ ClientSession session, String service) throws Exception {
+ PublicKeyIdentity result = getNextKey(session, service);
+ // This fixes SSHD-1231. Can be removed once we're using Apache MINA
+ // sshd > 2.8.0.
+ //
+ // See https://issues.apache.org/jira/browse/SSHD-1231
+ currentAlgorithms.clear();
+ return result;
+ }
+
+ private PublicKeyIdentity getNextKey(ClientSession session, String service)
+ throws Exception {
+ PublicKeyIdentity id = super.resolveAttemptedPublicKeyIdentity(session,
+ service);
+ if (addKeysToAgent && id != null && !(id instanceof KeyAgentIdentity)) {
+ KeyPair key = id.getKeyIdentity();
+ if (key != null && key.getPublic() != null
+ && key.getPrivate() != null) {
+ // We've just successfully loaded a key that wasn't in the
+ // agent. Add it to the agent.
+ //
+ // Keys are added after loading, as in OpenSSH. The alternative
+ // might be to add a key only after (partially) successful
+ // authentication?
+ PublicKey pk = key.getPublic();
+ String fingerprint = KeyUtils.getFingerPrint(pk);
+ String keyType = KeyUtils.getKeyType(key);
+ try {
+ // Check that the key is not in the agent already.
+ if (agentHasKey(pk)) {
+ return id;
+ }
+ if (askBeforeAdding
+ && (session instanceof JGitClientSession)) {
+ CredentialsProvider provider = ((JGitClientSession) session)
+ .getCredentialsProvider();
+ CredentialItem.YesNoType question = new CredentialItem.YesNoType(
+ format(SshdText
+ .get().pubkeyAuthAddKeyToAgentQuestion,
+ keyType, fingerprint));
+ boolean result = provider != null
+ && provider.supports(question)
+ && provider.get(getUri(), question);
+ if (!result || !question.getValue()) {
+ // Don't add the key.
+ return id;
+ }
+ }
+ SshAgentKeyConstraint[] rules = constraints;
+ if (pk instanceof SecurityKeyPublicKey && !StringUtils.isEmptyOrNull(skProvider)) {
+ rules = Arrays.copyOf(rules, rules.length + 1);
+ rules[rules.length - 1] =
+ new SshAgentKeyConstraint.FidoProviderExtension(skProvider);
+ }
+ // Unfortunately a comment associated with the key is lost
+ // by Apache MINA sshd, and there is also no way to get the
+ // original file name for keys loaded from a file. So add it
+ // without comment.
+ agent.addIdentity(key, null, rules);
+ } catch (IOException e) {
+ // Do not re-throw: we don't want authentication to fail if
+ // we cannot add the key to the agent.
+ log.error(
+ format(SshdText.get().pubkeyAuthAddKeyToAgentError,
+ keyType, fingerprint),
+ e);
+ // Note that as of Win32-OpenSSH 8.6 and Pageant 0.76,
+ // neither can handle key constraints. Pageant fails
+ // gracefully, not adding the key and returning
+ // SSH_AGENT_FAILURE. Win32-OpenSSH closes the connection
+ // without even returning a failure message, which violates
+ // the SSH agent protocol and makes all subsequent requests
+ // to the agent fail.
+ }
+ }
+ }
+ return id;
+ }
+
+ private boolean agentHasKey(PublicKey pk) throws IOException {
+ Iterable<? extends Map.Entry<PublicKey, String>> ids = agent
+ .getIdentities();
+ if (ids == null) {
+ return false;
+ }
+ Iterator<? extends Map.Entry<PublicKey, String>> iter = ids.iterator();
+ while (iter.hasNext()) {
+ if (KeyUtils.compareKeys(iter.next().getKey(), pk)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private URIish getUri() {
+ String uri = SshConstants.SSH_SCHEME + "://"; //$NON-NLS-1$
+ String userName = hostConfig.getUsername();
+ if (!StringUtils.isEmptyOrNull(userName)) {
+ uri += userName + '@';
+ }
+ uri += hostConfig.getHost();
+ int port = hostConfig.getPort();
+ if (port > 0 && port != SshConstants.SSH_DEFAULT_PORT) {
+ uri += ":" + port; //$NON-NLS-1$
+ }
+ try {
+ return new URIish(uri);
+ } catch (URISyntaxException e) {
+ log.error(e.getLocalizedMessage(), e);
+ }
+ return new URIish();
+ }
+
+ private SshAgent getAgent(ClientSession session) throws Exception {
+ FactoryManager manager = Objects.requireNonNull(
+ session.getFactoryManager(), "No session factory manager"); //$NON-NLS-1$
+ SshAgentFactory factory = manager.getAgentFactory();
+ if (factory == null) {
+ return null;
+ }
+ return factory.createClient(session, manager);
+ }
+
+ private void parseAddKeys(HostConfigEntry config) {
+ String value = config.getProperty(SshConstants.ADD_KEYS_TO_AGENT);
+ if (StringUtils.isEmptyOrNull(value)) {
+ addKeysToAgent = false;
+ return;
+ }
+ String[] values = value.split(","); //$NON-NLS-1$
+ List<SshAgentKeyConstraint> rules = new ArrayList<>(2);
+ switch (values[0]) {
+ case "yes": //$NON-NLS-1$
+ addKeysToAgent = true;
+ break;
+ case "no": //$NON-NLS-1$
+ addKeysToAgent = false;
+ break;
+ case "ask": //$NON-NLS-1$
+ addKeysToAgent = true;
+ askBeforeAdding = true;
+ break;
+ case "confirm": //$NON-NLS-1$
+ addKeysToAgent = true;
+ rules.add(SshAgentKeyConstraint.CONFIRM);
+ if (values.length > 1) {
+ int seconds = OpenSshConfigFile.timeSpec(values[1]);
+ if (seconds > 0) {
+ rules.add(new SshAgentKeyConstraint.LifeTime(seconds));
+ }
+ }
+ break;
+ default:
+ int seconds = OpenSshConfigFile.timeSpec(values[0]);
+ if (seconds > 0) {
+ addKeysToAgent = true;
+ rules.add(new SshAgentKeyConstraint.LifeTime(seconds));
+ }
+ break;
+ }
+ constraints = rules.toArray(new SshAgentKeyConstraint[0]);
+ }
+
+ @Override
+ protected void releaseKeys() throws IOException {
+ addKeysToAgent = false;
+ askBeforeAdding = false;
+ skProvider = null;
+ constraints = null;
+ try {
+ if (agent != null) {
+ try {
+ agent.close();
+ } finally {
+ agent = null;
+ }
+ }
+ } finally {
+ super.releaseKeys();
+ }
+ }
+
+ private class KeyIterator extends UserAuthPublicKeyIterator {
+
+ private Iterable<? extends Map.Entry<PublicKey, String>> agentKeys;
+
+ // If non-null, all the public keys from explicitly given key files. Any
+ // agent key not matching one of these public keys will be ignored in
+ // getIdentities().
+ private Collection<PublicKey> identityFiles;
+
+ public KeyIterator(ClientSession session,
+ SignatureFactoriesManager manager)
+ throws Exception {
+ super(session, manager);
+ }
+
+ private List<PublicKey> getExplicitKeys(
+ Collection<String> explicitFiles) {
+ if (explicitFiles == null) {
+ return null;
+ }
+ return explicitFiles.stream().map(s -> {
+ try {
+ Path p = Paths.get(s + ".pub"); //$NON-NLS-1$
+ if (Files.isRegularFile(p, LinkOption.NOFOLLOW_LINKS)) {
+ return AuthorizedKeyEntry.readAuthorizedKeys(p).get(0)
+ .resolvePublicKey(null,
+ PublicKeyEntryResolver.IGNORING);
+ }
+ } catch (InvalidPathException | IOException
+ | GeneralSecurityException e) {
+ log.warn(format(SshdText.get().cannotReadPublicKey, s), e);
+ }
+ return null;
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ }
+
+ @Override
+ protected Iterable<KeyAgentIdentity> initializeAgentIdentities(
+ ClientSession session) throws IOException {
+ if (agent == null) {
+ return null;
+ }
+ agentKeys = agent.getIdentities();
+ if (hostConfig != null && hostConfig.isIdentitiesOnly()) {
+ identityFiles = getExplicitKeys(hostConfig.getIdentities());
+ }
+ return () -> new Iterator<>() {
+
+ private final Iterator<? extends Map.Entry<PublicKey, String>> iter = agentKeys
+ .iterator();
+
+ private Map.Entry<PublicKey, String> next;
@Override
public boolean hasNext() {
- if (value != null) {
- return true;
- }
- PublicKeyIdentity next = null;
- while (original.hasNext()) {
- next = original.next();
- if (!(next instanceof KeyAgentIdentity)) {
- value = next;
+ while (next == null && iter.hasNext()) {
+ Map.Entry<PublicKey, String> val = iter.next();
+ PublicKey pk = val.getKey();
+ // This checks against all explicit keys for any agent
+ // key, but since identityFiles.size() is typically 1,
+ // it should be fine.
+ if (identityFiles == null || identityFiles.stream()
+ .anyMatch(k -> KeyUtils.compareKeys(k, pk))) {
+ next = val;
return true;
}
+ if (log.isTraceEnabled()) {
+ log.trace(
+ "Ignoring SSH agent {} key not in explicit IdentityFile in SSH config: {}", //$NON-NLS-1$
+ KeyUtils.getKeyType(pk),
+ KeyUtils.getFingerPrint(pk));
+ }
}
- return false;
+ return next != null;
}
@Override
- public PublicKeyIdentity next() {
- if (hasNext()) {
- PublicKeyIdentity result = value;
- value = null;
- return result;
+ public KeyAgentIdentity next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
}
- throw new NoSuchElementException();
+ KeyAgentIdentity result = new KeyAgentIdentity(agent,
+ next.getKey(), next.getValue());
+ next = null;
+ return result;
}
};
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
index 71e8e61585..72f0bdb6ee 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -87,6 +87,11 @@ public class JGitSshClient extends SshClient {
public static final AttributeKey<String> PREFERRED_AUTHENTICATIONS = new AttributeKey<>();
/**
+ * An attribute key for the home directory.
+ */
+ public static final AttributeKey<Path> HOME_DIRECTORY = new AttributeKey<>();
+
+ /**
* An attribute key for storing an alternate local address to connect to if
* a local forward from a ProxyJump ssh config is present. If set,
* {@link #connect(HostConfigEntry, AttributeRepository, SocketAddress)}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
index c51a75bc6f..2a725ea16a 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitUserInteraction.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -120,15 +120,16 @@ public class JGitUserInteraction implements UserInteraction {
return null;
}).filter(s -> s != null).toArray(String[]::new);
}
- // TODO What to throw to abort the connection/authentication process?
- // In UserAuthKeyboardInteractive.getUserResponses() it's clear that
- // returning null is valid and signifies "an error"; we'll try the
- // next authentication method. But if the user explicitly canceled,
- // then we don't want to try the next methods...
- //
- // Probably not a serious issue with the typical order of public-key,
- // keyboard-interactive, password.
- return null;
+ throw new AuthenticationCanceledException();
+ }
+
+ @Override
+ public String resolveAuthPasswordAttempt(ClientSession session)
+ throws Exception {
+ String[] results = interactive(session, null, null, "", //$NON-NLS-1$
+ new String[] { SshdText.get().passwordPrompt },
+ new boolean[] { false });
+ return (results == null || results.length == 0) ? null : results[0];
}
@Override
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
index 00ee62d6dd..39332d9fca 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/SshdText.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -29,7 +29,25 @@ public final class SshdText extends TranslationBundle {
// @formatter:off
/***/ public String authenticationCanceled;
/***/ public String authenticationOnClosedSession;
+ /***/ public String authGssApiAttempt;
+ /***/ public String authGssApiExhausted;
+ /***/ public String authGssApiFailure;
+ /***/ public String authGssApiNotTried;
+ /***/ public String authGssApiPartialSuccess;
+ /***/ public String authPasswordAttempt;
+ /***/ public String authPasswordChangeAttempt;
+ /***/ public String authPasswordExhausted;
+ /***/ public String authPasswordFailure;
+ /***/ public String authPasswordNotTried;
+ /***/ public String authPasswordPartialSuccess;
+ /***/ public String authPubkeyAttempt;
+ /***/ public String authPubkeyAttemptAgent;
+ /***/ public String authPubkeyExhausted;
+ /***/ public String authPubkeyFailure;
+ /***/ public String authPubkeyNoKeys;
+ /***/ public String authPubkeyPartialSuccess;
/***/ public String closeListenerFailed;
+ /***/ public String cannotReadPublicKey;
/***/ public String configInvalidPath;
/***/ public String configInvalidPattern;
/***/ public String configInvalidPositive;
@@ -98,6 +116,8 @@ public final class SshdText extends TranslationBundle {
/***/ public String proxySocksUnexpectedMessage;
/***/ public String proxySocksUnexpectedVersion;
/***/ public String proxySocksUsernameTooLong;
+ /***/ public String pubkeyAuthAddKeyToAgentError;
+ /***/ public String pubkeyAuthAddKeyToAgentQuestion;
/***/ public String pubkeyAuthWrongCommand;
/***/ public String pubkeyAuthWrongKey;
/***/ public String pubkeyAuthWrongSignatureAlgorithm;
@@ -106,9 +126,13 @@ public final class SshdText extends TranslationBundle {
/***/ public String serverIdWithNul;
/***/ public String sessionCloseFailed;
/***/ public String sessionWithoutUsername;
+ /***/ public String sshAgentEdDSAFormatError;
+ /***/ public String sshAgentPayloadLengthError;
/***/ public String sshAgentReplyLengthError;
/***/ public String sshAgentReplyUnexpected;
/***/ public String sshAgentShortReadBuffer;
+ /***/ public String sshAgentUnknownKey;
+ /***/ public String sshAgentWrongKeyLength;
/***/ public String sshAgentWrongNumberOfKeys;
/***/ public String sshClosingDown;
/***/ public String sshCommandTimeout;
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java
index 1ed2ab9d78..a0ffd540f2 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/JGitSshAgentFactory.java
@@ -17,10 +17,14 @@ import java.util.List;
import org.apache.sshd.agent.SshAgent;
import org.apache.sshd.agent.SshAgentFactory;
import org.apache.sshd.agent.SshAgentServer;
+import org.apache.sshd.client.config.hosts.HostConfigEntry;
import org.apache.sshd.common.FactoryManager;
import org.apache.sshd.common.channel.ChannelFactory;
import org.apache.sshd.common.session.ConnectionService;
+import org.apache.sshd.common.session.Session;
import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.transport.sshd.JGitClientSession;
+import org.eclipse.jgit.transport.SshConstants;
import org.eclipse.jgit.transport.sshd.agent.ConnectorFactory;
/**
@@ -49,18 +53,24 @@ public class JGitSshAgentFactory implements SshAgentFactory {
@Override
public List<ChannelFactory> getChannelForwardingFactories(
FactoryManager manager) {
- // No agent forwarding supported yet.
+ // No agent forwarding supported.
return Collections.emptyList();
}
@Override
- public SshAgent createClient(FactoryManager manager) throws IOException {
- // sshd 2.8.0 will pass us the session here. At that point, we can get
- // the HostConfigEntry and extract and handle the IdentityAgent setting.
- // For now, pass null to let the ConnectorFactory do its default
- // behavior (Pageant on Windows, SSH_AUTH_SOCK on Unixes with the
- // jgit-builtin factory).
- return new SshAgentClient(factory.create(null, homeDir));
+ public SshAgent createClient(Session session, FactoryManager manager)
+ throws IOException {
+ String identityAgent = null;
+ if (session instanceof JGitClientSession) {
+ HostConfigEntry hostConfig = ((JGitClientSession) session)
+ .getHostConfigEntry();
+ identityAgent = hostConfig.getProperty(SshConstants.IDENTITY_AGENT,
+ null);
+ }
+ if (SshConstants.NONE.equals(identityAgent)) {
+ return null;
+ }
+ return new SshAgentClient(factory.create(identityAgent, homeDir));
}
@Override
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java
index 08483e4c20..cbcb4d240e 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/agent/SshAgentClient.java
@@ -11,10 +11,12 @@ package org.eclipse.jgit.internal.transport.sshd.agent;
import java.io.IOException;
import java.security.KeyPair;
+import java.security.PrivateKey;
import java.security.PublicKey;
import java.text.MessageFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -22,21 +24,27 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.sshd.agent.SshAgent;
import org.apache.sshd.agent.SshAgentConstants;
+import org.apache.sshd.agent.SshAgentKeyConstraint;
import org.apache.sshd.common.SshException;
import org.apache.sshd.common.config.keys.KeyUtils;
+import org.apache.sshd.common.keyprovider.KeyPairProvider;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.buffer.Buffer;
import org.apache.sshd.common.util.buffer.BufferException;
import org.apache.sshd.common.util.buffer.BufferUtils;
import org.apache.sshd.common.util.buffer.ByteArrayBuffer;
+import org.apache.sshd.common.util.buffer.keys.BufferPublicKeyParser;
+import org.apache.sshd.common.util.io.der.DERParser;
import org.eclipse.jgit.internal.transport.sshd.SshdText;
import org.eclipse.jgit.transport.sshd.agent.Connector;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
- * A client for an SSH2 agent. This client supports only querying identities and
- * signature requests.
+ * A client for an SSH2 agent. This client supports querying identities,
+ * signature requests, and adding keys to an agent (with or without
+ * constraints). Removing keys is not supported, and the older SSH1 protocol is
+ * not supported.
*
* @see <a href="https://tools.ietf.org/html/draft-miller-ssh-agent-04">SSH
* Agent Protocol, RFC draft</a>
@@ -72,11 +80,18 @@ public class SshAgentClient implements SshAgent {
}
return false;
}
- boolean connected = connector != null && connector.connect();
- if (!connected) {
+ boolean connected;
+ try {
+ connected = connector != null && connector.connect();
+ if (!connected && debugging) {
+ LOG.debug("No SSH agent"); //$NON-NLS-1$
+ }
+ } catch (IOException e) {
+ // Agent not running?
if (debugging) {
- LOG.debug("No SSH agent (SSH_AUTH_SOCK not set)"); //$NON-NLS-1$
+ LOG.debug("No SSH agent", e); //$NON-NLS-1$
}
+ throw e;
}
return connected;
}
@@ -127,14 +142,17 @@ public class SshAgentClient implements SshAgent {
List<Map.Entry<PublicKey, String>> keys = new ArrayList<>(
numberOfKeys);
for (int i = 0; i < numberOfKeys; i++) {
- PublicKey key = reply.getPublicKey();
+ PublicKey key = readKey(reply);
String comment = reply.getString();
- if (tracing) {
- LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$
- KeyUtils.getKeyType(key),
- KeyUtils.getFingerPrint(key), comment);
+ if (key != null) {
+ if (tracing) {
+ LOG.trace("Got SSH agent {} key: {} {}", //$NON-NLS-1$
+ KeyUtils.getKeyType(key),
+ KeyUtils.getFingerPrint(key), comment);
+ }
+ keys.add(new AbstractMap.SimpleImmutableEntry<>(key,
+ comment));
}
- keys.add(new AbstractMap.SimpleImmutableEntry<>(key, comment));
}
return keys;
} catch (BufferException e) {
@@ -216,6 +234,222 @@ public class SshAgentClient implements SshAgent {
}
}
+ @Override
+ public void addIdentity(KeyPair key, String comment,
+ SshAgentKeyConstraint... constraints) throws IOException {
+ boolean debugging = LOG.isDebugEnabled();
+ if (!open(debugging)) {
+ return;
+ }
+
+ // Neither Pageant 0.76 nor Win32-OpenSSH 8.6 support command
+ // SSH2_AGENTC_ADD_ID_CONSTRAINED. Adding a key with constraints will
+ // fail. The only work-around for users is not to use "confirm" or "time
+ // spec" with AddKeysToAgent, and not to use sk-* keys.
+ //
+ // With a true OpenSSH SSH agent, key constraints work.
+ byte cmd = (constraints != null && constraints.length > 0)
+ ? SshAgentConstants.SSH2_AGENTC_ADD_ID_CONSTRAINED
+ : SshAgentConstants.SSH2_AGENTC_ADD_IDENTITY;
+ byte[] message = null;
+ ByteArrayBuffer msg = new ByteArrayBuffer();
+ try {
+ msg.putInt(0);
+ msg.putByte(cmd);
+ String keyType = KeyUtils.getKeyType(key);
+ if (KeyPairProvider.SSH_ED25519.equals(keyType)) {
+ // Apache MINA sshd 2.8.0 lacks support for writing ed25519
+ // private keys to a buffer.
+ putEd25519Key(msg, key);
+ } else {
+ msg.putKeyPair(key);
+ }
+ msg.putString(comment == null ? "" : comment); //$NON-NLS-1$
+ if (constraints != null) {
+ for (SshAgentKeyConstraint constraint : constraints) {
+ constraint.put(msg);
+ }
+ }
+ if (debugging) {
+ LOG.debug(
+ "addIdentity: adding {} key {} to SSH agent; comment {}", //$NON-NLS-1$
+ keyType, KeyUtils.getFingerPrint(key.getPublic()),
+ comment);
+ }
+ message = msg.getCompactData();
+ } finally {
+ // The message contains the private key data, so clear intermediary
+ // data ASAP.
+ msg.clear();
+ }
+ Buffer reply;
+ try {
+ reply = rpc(cmd, message);
+ } finally {
+ Arrays.fill(message, (byte) 0);
+ }
+ int replyLength = reply.available();
+ if (replyLength != 1) {
+ throw new SshException(MessageFormat.format(
+ SshdText.get().sshAgentReplyUnexpected,
+ MessageFormat.format(
+ SshdText.get().sshAgentPayloadLengthError,
+ Integer.valueOf(1), Integer.valueOf(replyLength))));
+
+ }
+ cmd = reply.getByte();
+ if (cmd != SshAgentConstants.SSH_AGENT_SUCCESS) {
+ throw new SshException(
+ MessageFormat.format(SshdText.get().sshAgentReplyUnexpected,
+ SshAgentConstants.getCommandMessageName(cmd)));
+ }
+ }
+
+ /**
+ * Writes an ed25519 {@link KeyPair} to a {@link Buffer}. OpenSSH specifies
+ * that it expects the 32 public key bytes, followed by 64 bytes formed by
+ * concatenating the 32 private key bytes with the 32 public key bytes.
+ *
+ * @param msg
+ * {@link Buffer} to write to
+ * @param key
+ * {@link KeyPair} to write
+ * @throws IOException
+ * if the private key cannot be written
+ */
+ private static void putEd25519Key(Buffer msg, KeyPair key)
+ throws IOException {
+ Buffer tmp = new ByteArrayBuffer(36);
+ tmp.putRawPublicKeyBytes(key.getPublic());
+ byte[] publicBytes = tmp.getBytes();
+ msg.putString(KeyPairProvider.SSH_ED25519);
+ msg.putBytes(publicBytes);
+ // Next is the concatenation of the 32 byte private key value with the
+ // 32 bytes of the public key.
+ PrivateKey pk = key.getPrivate();
+ String format = pk.getFormat();
+ if (!"PKCS#8".equalsIgnoreCase(format)) { //$NON-NLS-1$
+ throw new IOException(MessageFormat
+ .format(SshdText.get().sshAgentEdDSAFormatError, format));
+ }
+ byte[] privateBytes = null;
+ byte[] encoded = pk.getEncoded();
+ try {
+ privateBytes = asn1Parse(encoded, 32);
+ byte[] combined = Arrays.copyOf(privateBytes, 64);
+ Arrays.fill(privateBytes, (byte) 0);
+ privateBytes = combined;
+ System.arraycopy(publicBytes, 0, privateBytes, 32, 32);
+ msg.putBytes(privateBytes);
+ } finally {
+ if (privateBytes != null) {
+ Arrays.fill(privateBytes, (byte) 0);
+ }
+ Arrays.fill(encoded, (byte) 0);
+ }
+ }
+
+ /**
+ * Extracts the private key bytes from an encoded ed25519 private key by
+ * parsing the bytes as ASN.1 according to RFC 5958 (PKCS #8 encoding):
+ *
+ * <pre>
+ * OneAsymmetricKey ::= SEQUENCE {
+ * version Version,
+ * privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
+ * privateKey PrivateKey,
+ * ...
+ * }
+ *
+ * Version ::= INTEGER
+ * PrivateKeyAlgorithmIdentifier ::= AlgorithmIdentifier
+ * PrivateKey ::= OCTET STRING
+ *
+ * AlgorithmIdentifier ::= SEQUENCE {
+ * algorithm OBJECT IDENTIFIER,
+ * parameters ANY DEFINED BY algorithm OPTIONAL
+ * }
+ * </pre>
+ * <p>
+ * and RFC 8410: "... when encoding a OneAsymmetricKey object, the private
+ * key is wrapped in a CurvePrivateKey object and wrapped by the OCTET
+ * STRING of the 'privateKey' field."
+ * </p>
+ *
+ * <pre>
+ * CurvePrivateKey ::= OCTET STRING
+ * </pre>
+ *
+ * @param encoded
+ * encoded private key to extract the private key bytes from
+ * @param n
+ * number of bytes expected
+ * @return the extracted private key bytes; of length {@code n}
+ * @throws IOException
+ * if the private key cannot be extracted
+ * @see <a href="https://tools.ietf.org/html/rfc5958">RFC 5958</a>
+ * @see <a href="https://tools.ietf.org/html/rfc8410">RFC 8410</a>
+ */
+ private static byte[] asn1Parse(byte[] encoded, int n) throws IOException {
+ byte[] privateKey = null;
+ try (DERParser byteParser = new DERParser(encoded);
+ DERParser oneAsymmetricKey = byteParser.readObject()
+ .createParser()) {
+ oneAsymmetricKey.readObject(); // skip version
+ oneAsymmetricKey.readObject(); // skip algorithm identifier
+ privateKey = oneAsymmetricKey.readObject().getValue();
+ // The last n bytes of this must be the private key bytes
+ return Arrays.copyOfRange(privateKey,
+ privateKey.length - n, privateKey.length);
+ } finally {
+ if (privateKey != null) {
+ Arrays.fill(privateKey, (byte) 0);
+ }
+ }
+ }
+
+ /**
+ * A safe version of {@link Buffer#getPublicKey()}. Upon return the
+ * buffers's read position is always after the key blob; any exceptions
+ * thrown by trying to read the key are logged and <em>not</em> propagated.
+ * <p>
+ * This is needed because an SSH agent might contain and deliver keys that
+ * we cannot handle (for instance ed448 keys).
+ * </p>
+ *
+ * @param buffer
+ * to read the key from
+ * @return the {@link PublicKey}, or {@code null} if the key could not be
+ * read
+ * @throws BufferException
+ * if the length of the key blob cannot be read or is corrupted
+ */
+ private static PublicKey readKey(Buffer buffer) throws BufferException {
+ int endOfBuffer = buffer.wpos();
+ int keyLength = buffer.getInt();
+ int afterKey = buffer.rpos() + keyLength;
+ if (keyLength <= 0 || afterKey > endOfBuffer) {
+ throw new BufferException(
+ MessageFormat.format(SshdText.get().sshAgentWrongKeyLength,
+ Integer.toString(keyLength),
+ Integer.toString(buffer.rpos()),
+ Integer.toString(endOfBuffer)));
+ }
+ // Limit subsequent reads to the public key blob
+ buffer.wpos(afterKey);
+ try {
+ return buffer.getRawPublicKey(BufferPublicKeyParser.DEFAULT);
+ } catch (Exception e) {
+ LOG.warn(SshdText.get().sshAgentUnknownKey, e);
+ return null;
+ } finally {
+ // Restore real buffer end
+ buffer.wpos(endOfBuffer);
+ // Set the read position to after this key, even if failed
+ buffer.rpos(afterKey);
+ }
+ }
+
private Buffer rpc(byte command, byte[] message) throws IOException {
return new ByteArrayBuffer(connector.rpc(command, message));
}
@@ -230,11 +464,6 @@ public class SshAgentClient implements SshAgent {
}
@Override
- public void addIdentity(KeyPair key, String comment) throws IOException {
- throw new UnsupportedOperationException();
- }
-
- @Override
public void removeIdentity(PublicKey key) throws IOException {
throw new UnsupportedOperationException();
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
index c270b44956..b94ccc6d4f 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSession.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -51,6 +51,9 @@ import org.apache.sshd.sftp.client.SftpClientFactory;
import org.apache.sshd.sftp.common.SftpException;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.errors.TransportException;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
+import org.eclipse.jgit.internal.transport.sshd.AuthenticationLogger;
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
import org.eclipse.jgit.internal.transport.sshd.SshdText;
import org.eclipse.jgit.transport.FtpChannel;
@@ -118,6 +121,7 @@ public class SshdSession implements RemoteSession2 {
ClientSession resultSession = null;
ClientSession proxySession = null;
PortForwardingTracker portForward = null;
+ AuthenticationLogger authLog = null;
try {
if (!hops.isEmpty()) {
URIish hop = hops.remove(0);
@@ -138,7 +142,11 @@ public class SshdSession implements RemoteSession2 {
JGitSshClient.LOCAL_FORWARD_ADDRESS,
portForward.getBoundAddress());
}
- resultSession = connect(hostConfig, context, timeout);
+ int timeoutInSec = OpenSshConfigFile.timeSpec(
+ hostConfig.getProperty(SshConstants.CONNECT_TIMEOUT));
+ resultSession = connect(hostConfig, context,
+ timeoutInSec > 0 ? Duration.ofSeconds(timeoutInSec)
+ : timeout);
if (proxySession != null) {
final PortForwardingTracker tracker = portForward;
final ClientSession pSession = proxySession;
@@ -160,6 +168,7 @@ public class SshdSession implements RemoteSession2 {
resultSession.addCloseFutureListener(listener);
}
// Authentication timeout is by default 2 minutes.
+ authLog = new AuthenticationLogger(resultSession);
resultSession.auth().verify(resultSession.getAuthTimeout());
return resultSession;
} catch (IOException e) {
@@ -168,15 +177,32 @@ public class SshdSession implements RemoteSession2 {
close(resultSession, e);
if (e instanceof SshException && ((SshException) e)
.getDisconnectCode() == SSH2_DISCONNECT_NO_MORE_AUTH_METHODS_AVAILABLE) {
- // Ensure the user gets to know on which URI the authentication
- // was denied.
+ String message = format(SshdText.get().loginDenied, host,
+ Integer.toString(port));
throw new TransportException(target,
- format(SshdText.get().loginDenied, host,
- Integer.toString(port)),
- e);
+ withAuthLog(message, authLog), e);
+ } else if (e instanceof SshException && e
+ .getCause() instanceof AuthenticationCanceledException) {
+ String message = e.getCause().getMessage();
+ throw new TransportException(target,
+ withAuthLog(message, authLog), e.getCause());
}
throw e;
+ } finally {
+ if (authLog != null) {
+ authLog.clear();
+ }
+ }
+ }
+
+ private String withAuthLog(String message, AuthenticationLogger authLog) {
+ if (authLog != null) {
+ String log = String.join(System.lineSeparator(), authLog.getLog());
+ if (!log.isEmpty()) {
+ return message + System.lineSeparator() + log;
+ }
}
+ return message;
}
private ClientSession connect(HostConfigEntry config,
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
index 58cf8e1ddd..c792c1889c 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2018, 2021 Thomas Wolf <thomas.wolf@paranor.ch> and others
+ * Copyright (C) 2018, 2022 Thomas Wolf <thomas.wolf@paranor.ch> 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
@@ -13,6 +13,7 @@ import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
+import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.security.KeyPair;
import java.time.Duration;
@@ -34,7 +35,6 @@ import org.apache.sshd.client.auth.UserAuthFactory;
import org.apache.sshd.client.auth.keyboard.UserAuthKeyboardInteractiveFactory;
import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
import org.apache.sshd.common.NamedFactory;
-import org.apache.sshd.common.SshException;
import org.apache.sshd.common.compression.BuiltinCompressions;
import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.loader.openssh.kdf.BCryptKdfOptions;
@@ -44,7 +44,6 @@ import org.apache.sshd.common.signature.Signature;
import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
-import org.eclipse.jgit.internal.transport.sshd.AuthenticationCanceledException;
import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
import org.eclipse.jgit.internal.transport.sshd.JGitPasswordAuthFactory;
@@ -243,6 +242,12 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
JGitSshClient.PREFERRED_AUTHENTICATIONS,
defaultAuths);
}
+ try {
+ jgitClient.setAttribute(JGitSshClient.HOME_DIRECTORY,
+ home.getAbsoluteFile().toPath());
+ } catch (SecurityException | InvalidPathException e) {
+ // Ignore
+ }
// Other things?
return client;
});
@@ -255,13 +260,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
if (e instanceof TransportException) {
throw (TransportException) e;
}
- Throwable cause = e;
- if (e instanceof SshException && e
- .getCause() instanceof AuthenticationCanceledException) {
- // Results in a nicer error message
- cause = e.getCause();
- }
- throw new TransportException(uri, cause.getMessage(), cause);
+ throw new TransportException(uri, e.getMessage(), e);
}
}

Back to the top