summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF2
-rw-r--r--org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties5
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/CachingKeyPairProvider.java8
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/EncryptedFileKeyPairProvider.java159
-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/SshdText.java5
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java195
-rw-r--r--org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/RepeatingFilePasswordProvider.java120
-rw-r--r--org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java47
-rw-r--r--org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestHarness.java28
10 files changed, 555 insertions, 21 deletions
diff --git a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
index 987f8dcdce..caeff53634 100644
--- a/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
+++ b/org.eclipse.jgit.ssh.apache/META-INF/MANIFEST.MF
@@ -48,6 +48,7 @@ Import-Package: org.apache.sshd.agent;version="[2.0.0,2.1.0)",
org.apache.sshd.common.channel;version="[2.0.0,2.1.0)",
org.apache.sshd.common.compression;version="[2.0.0,2.1.0)",
org.apache.sshd.common.config.keys;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.config.keys.loader;version="[2.0.0,2.1.0)",
org.apache.sshd.common.digest;version="[2.0.0,2.1.0)",
org.apache.sshd.common.forward;version="[2.0.0,2.1.0)",
org.apache.sshd.common.future;version="[2.0.0,2.1.0)",
@@ -66,6 +67,7 @@ Import-Package: org.apache.sshd.agent;version="[2.0.0,2.1.0)",
org.apache.sshd.common.util.io;version="[2.0.0,2.1.0)",
org.apache.sshd.common.util.logging;version="[2.0.0,2.1.0)",
org.apache.sshd.common.util.net;version="[2.0.0,2.1.0)",
+ org.apache.sshd.common.util.security;version="[2.0.0,2.1.0)",
org.apache.sshd.server.auth;version="[2.0.0,2.1.0)",
org.eclipse.jgit.annotations;version="[5.2.0,5.3.0)",
org.eclipse.jgit.errors;version="[5.2.0,5.3.0)",
diff --git a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
index 369c9784d0..12afedd8b2 100644
--- a/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
+++ b/org.eclipse.jgit.ssh.apache/resources/org/eclipse/jgit/internal/transport/sshd/SshdText.properties
@@ -10,8 +10,13 @@ gssapiFailure=GSS-API error for mechanism OID {0}
gssapiInitFailure=GSS-API initialization failure for mechanism {0}
gssapiUnexpectedMechanism=Server {0} replied with unknown mechanism name ''{1}'' in {2} authentication
gssapiUnexpectedMessage=Received unexpected ssh message {1} in {0} authentication
+identityFileCannotDecrypt=Given passphrase cannot decrypt identity {0}
+identityFileNoKey=No keys found in identity {0}
+identityFileMultipleKeys=Multiple key pairs found in identity {0}
+identityFileUnsupportedFormat=Unsupported format in identity {0}
keyEncryptedMsg=Key ''{0}'' is encrypted. Enter the passphrase to decrypt it.
keyEncryptedPrompt=Passphrase
+keyEncryptedRetry=Encrypted key ''{0}'' could not be decrypted. Enter the passphrase again.
keyLoadFailed=Could not load key ''{0}''
knownHostsCouldNotUpdate=Could not update known hosts file {0}
knownHostsFileLockedRead=Could not read known hosts file (locked) {0}
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 dcd17af2ff..ad2ff5256c 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
@@ -56,20 +56,20 @@ import java.util.List;
import java.util.NoSuchElementException;
import java.util.concurrent.CancellationException;
-import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.eclipse.jgit.transport.sshd.KeyCache;
/**
- * A {@link FileKeyPairProvider} that uses an external {@link KeyCache}.
+ * A {@link EncryptedFileKeyPairProvider} that uses an external
+ * {@link KeyCache}.
*/
-public class CachingKeyPairProvider extends FileKeyPairProvider {
+public class CachingKeyPairProvider extends EncryptedFileKeyPairProvider {
private final KeyCache cache;
/**
* Creates a new {@link CachingKeyPairProvider} using the given
* {@link KeyCache}. If the cache is {@code null}, this is a simple
- * {@link FileKeyPairProvider}.
+ * {@link EncryptedFileKeyPairProvider}.
*
* @param paths
* to load keys from
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/EncryptedFileKeyPairProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/EncryptedFileKeyPairProvider.java
new file mode 100644
index 0000000000..2e201d8828
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/EncryptedFileKeyPairProvider.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.internal.transport.sshd;
+
+import static java.text.MessageFormat.format;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Path;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
+import java.security.KeyPair;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.security.auth.DestroyFailedException;
+
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
+import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
+import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.eclipse.jgit.transport.sshd.RepeatingFilePasswordProvider;
+import org.eclipse.jgit.transport.sshd.RepeatingFilePasswordProvider.ResourceDecodeResult;
+
+/**
+ * A {@link FileKeyPairProvider} that asks repeatedly for a passphrase for an
+ * encrypted private key if the {@link FilePasswordProvider} is a
+ * {@link RepeatingFilePasswordProvider}.
+ */
+public class EncryptedFileKeyPairProvider extends FileKeyPairProvider {
+
+ // TODO: remove this class once we're based on sshd > 2.1.0. See upstream
+ // issue SSHD-850 https://issues.apache.org/jira/browse/SSHD-850 and commit
+ // https://github.com/apache/mina-sshd/commit/f19bd2e34
+
+ /**
+ * Creates a new {@link EncryptedFileKeyPairProvider} for the given
+ * {@link Path}s.
+ *
+ * @param paths
+ * to read keys from
+ */
+ public EncryptedFileKeyPairProvider(List<Path> paths) {
+ super(paths);
+ }
+
+ @Override
+ protected KeyPair doLoadKey(String resourceKey, InputStream inputStream,
+ FilePasswordProvider provider)
+ throws IOException, GeneralSecurityException {
+ if (!(provider instanceof RepeatingFilePasswordProvider)) {
+ return super.doLoadKey(resourceKey, inputStream, provider);
+ }
+ KeyPairResourceParser parser = SecurityUtils.getKeyPairResourceParser();
+ if (parser == null) {
+ // This is an internal configuration error, thus no translation.
+ throw new NoSuchProviderException(
+ "No registered key-pair resource parser"); //$NON-NLS-1$
+ }
+ RepeatingFilePasswordProvider realProvider = (RepeatingFilePasswordProvider) provider;
+ // Read the stream now so that we can process the content several
+ // times.
+ List<String> lines = IoUtils.readAllLines(inputStream);
+ Collection<KeyPair> ids = null;
+ while (ids == null) {
+ try {
+ ids = parser.loadKeyPairs(resourceKey, realProvider, lines);
+ realProvider.handleDecodeAttemptResult(resourceKey, "", null); //$NON-NLS-1$
+ // No exception; success. Exit the loop even if ids is still
+ // null!
+ break;
+ } catch (IOException | GeneralSecurityException
+ | RuntimeException e) {
+ ResourceDecodeResult loadResult = realProvider
+ .handleDecodeAttemptResult(resourceKey, "", e); //$NON-NLS-1$
+ if (loadResult == null
+ || loadResult == ResourceDecodeResult.TERMINATE) {
+ throw e;
+ } else if (loadResult == ResourceDecodeResult.RETRY) {
+ continue;
+ }
+ // IGNORE doesn't make any sense here, but OK, let's ignore it.
+ // ids == null, so we'll throw an exception below.
+ break;
+ }
+ }
+ if (ids == null) {
+ // The javadoc on loadKeyPairs says it might return null if no
+ // key pair found. Bad API.
+ throw new InvalidKeyException(
+ format(SshdText.get().identityFileNoKey, resourceKey));
+ }
+ Iterator<KeyPair> keys = ids.iterator();
+ if (!keys.hasNext()) {
+ throw new InvalidKeyException(format(
+ SshdText.get().identityFileUnsupportedFormat, resourceKey));
+ }
+ KeyPair result = keys.next();
+ if (keys.hasNext()) {
+ log.warn(format(SshdText.get().identityFileMultipleKeys,
+ resourceKey));
+ keys.forEachRemaining(k -> {
+ PrivateKey pk = k.getPrivate();
+ if (pk != null) {
+ try {
+ pk.destroy();
+ } catch (DestroyFailedException e) {
+ // Ignore
+ }
+ }
+ });
+ }
+ 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 27cf05077a..d3289259ed 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
@@ -65,6 +65,7 @@ import org.apache.sshd.client.future.ConnectFuture;
import org.apache.sshd.client.future.DefaultConnectFuture;
import org.apache.sshd.client.session.ClientSessionImpl;
import org.apache.sshd.client.session.SessionFactory;
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.future.SshFutureListener;
import org.apache.sshd.common.io.IoConnectFuture;
import org.apache.sshd.common.io.IoSession;
@@ -75,6 +76,7 @@ import org.apache.sshd.common.util.ValidateUtils;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshConstants;
import org.eclipse.jgit.transport.sshd.KeyCache;
+import org.eclipse.jgit.transport.sshd.RepeatingFilePasswordProvider;
/**
* Customized {@link SshClient} for JGit. It creates specialized
@@ -195,6 +197,11 @@ public class JGitSshClient extends SshClient {
int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig);
session.getProperties().put(PASSWORD_PROMPTS,
Integer.valueOf(numberOfPasswordPrompts));
+ FilePasswordProvider provider = getFilePasswordProvider();
+ if (provider instanceof RepeatingFilePasswordProvider) {
+ ((RepeatingFilePasswordProvider) provider)
+ .setAttempts(numberOfPasswordPrompts);
+ }
FileKeyPairProvider ourConfiguredKeysProvider = null;
List<Path> identities = hostConfig.getIdentities().stream()
.map(s -> {
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 e7e5d8fcce..bd9b2a2544 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
@@ -30,8 +30,13 @@ public final class SshdText extends TranslationBundle {
/***/ public String gssapiInitFailure;
/***/ public String gssapiUnexpectedMechanism;
/***/ public String gssapiUnexpectedMessage;
+ /***/ public String identityFileCannotDecrypt;
+ /***/ public String identityFileNoKey;
+ /***/ public String identityFileMultipleKeys;
+ /***/ public String identityFileUnsupportedFormat;
/***/ public String keyEncryptedMsg;
/***/ public String keyEncryptedPrompt;
+ /***/ public String keyEncryptedRetry;
/***/ public String keyLoadFailed;
/***/ public String knownHostsCouldNotUpdate;
/***/ public String knownHostsFileLockedRead;
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java
index 5a0bd7fdc8..231d3f4eb3 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java
@@ -46,26 +46,99 @@ import static java.text.MessageFormat.format;
import java.io.IOException;
import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.security.InvalidKeyException;
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.concurrent.CancellationException;
-import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.eclipse.jgit.annotations.NonNull;
import org.eclipse.jgit.internal.transport.sshd.SshdText;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;
/**
- * A {@link FilePasswordProvider} based on a {@link CredentialsProvider}.
+ * A {@link RepeatingFilePasswordProvider} based on a
+ * {@link CredentialsProvider}.
*
* @since 5.2
*/
-public class IdentityPasswordProvider implements FilePasswordProvider {
+public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
private CredentialsProvider provider;
/**
+ * The number of times to ask successively for a password for a given
+ * identity resource.
+ */
+ private int attempts = 1;
+
+ /**
+ * A simple state object for repeated attempts to get a password for a
+ * resource.
+ */
+ protected static class State {
+
+ private int count = 0;
+
+ private char[] password;
+
+ /**
+ * Obtains the current count. The initial count is zero.
+ *
+ * @return the count
+ */
+ public int getCount() {
+ return count;
+ }
+
+ /**
+ * Increments the current count. Should be called for each new attempt
+ * to get a password.
+ *
+ * @return the incremented count.
+ */
+ public int incCount() {
+ return ++count;
+ }
+
+ /**
+ * Remembers the password.
+ *
+ * @param password
+ * the password
+ */
+ public void setPassword(char[] password) {
+ if (this.password != null) {
+ Arrays.fill(this.password, '\000');
+ }
+ if (password != null) {
+ this.password = password.clone();
+ } else {
+ this.password = null;
+ }
+ }
+
+ /**
+ * Retrieves the password from the current attempt.
+ *
+ * @return the password, or {@code null} if none was obtained
+ */
+ public char[] getPassword() {
+ return password;
+ }
+ }
+
+ /**
+ * Counts per resource key.
+ */
+ private final Map<String, State> current = new HashMap<>();
+
+ /**
* Creates a new {@link IdentityPasswordProvider} to get the passphrase for
* an encrypted identity.
*
@@ -76,6 +149,56 @@ public class IdentityPasswordProvider implements FilePasswordProvider {
this.provider = provider;
}
+ @Override
+ public void setAttempts(int numberOfPasswordPrompts) {
+ RepeatingFilePasswordProvider.super.setAttempts(
+ numberOfPasswordPrompts);
+ attempts = numberOfPasswordPrompts;
+ }
+
+ @Override
+ public int getAttempts() {
+ return Math.max(1, attempts);
+ }
+
+ @Override
+ public String getPassword(String resourceKey) throws IOException {
+ char[] pass = getPassword(resourceKey,
+ current.computeIfAbsent(resourceKey, r -> new State()));
+ if (pass == null) {
+ return null;
+ }
+ try {
+ return new String(pass);
+ } finally {
+ Arrays.fill(pass, '\000');
+ }
+ }
+
+ /**
+ * Retrieves a password to decrypt a private key.
+ *
+ * @param resourceKey
+ * identifying the resource to obtain a password for
+ * @param state
+ * encapsulating state information about attempts to get the
+ * password
+ * @return the password, or {@code null} or the empty string if none
+ * available.
+ * @throws IOException
+ * if an error occurs
+ */
+ protected char[] getPassword(String resourceKey, @NonNull State state)
+ throws IOException {
+ state.setPassword(null);
+ state.incCount();
+ String message = state.count == 1 ? SshdText.get().keyEncryptedMsg
+ : SshdText.get().keyEncryptedRetry;
+ char[] pass = getPassword(resourceKey, message);
+ state.setPassword(pass);
+ return pass;
+ }
+
/**
* Creates a {@link URIish} from a given string. The
* {@link CredentialsProvider} uses uris as resource identifications.
@@ -92,15 +215,14 @@ public class IdentityPasswordProvider implements FilePasswordProvider {
}
}
- @Override
- public String getPassword(String resourceKey) throws IOException {
+ private char[] getPassword(String resourceKey, String message) {
if (provider == null) {
return null;
}
URIish file = toUri(resourceKey);
List<CredentialItem> items = new ArrayList<>(2);
items.add(new CredentialItem.InformationalMessage(
- format(SshdText.get().keyEncryptedMsg, resourceKey)));
+ format(message, resourceKey)));
CredentialItem.Password password = new CredentialItem.Password(
SshdText.get().keyEncryptedPrompt);
items.add(password);
@@ -111,10 +233,69 @@ public class IdentityPasswordProvider implements FilePasswordProvider {
throw new CancellationException(
SshdText.get().authenticationCanceled);
}
- return new String(pass);
+ return pass.clone();
} finally {
password.clear();
}
}
+ /**
+ * Invoked to inform the password provider about the decoding result.
+ *
+ * @param resourceKey
+ * the resource key
+ * @param state
+ * associated with this key
+ * @param password
+ * the password that was attempted
+ * @param err
+ * the attempt result - {@code null} for success
+ * @return how to proceed in case of error
+ * @throws IOException
+ * @throws GeneralSecurityException
+ * @see #handleDecodeAttemptResult(String, String, Exception)
+ */
+ protected ResourceDecodeResult handleDecodeAttemptResult(String resourceKey,
+ State state, char[] password, Exception err)
+ throws IOException, GeneralSecurityException {
+ if (err == null) {
+ return null;
+ } else if (err instanceof GeneralSecurityException) {
+ throw new InvalidKeyException(
+ format(SshdText.get().identityFileCannotDecrypt,
+ resourceKey),
+ err);
+ } else {
+ // Unencrypted key (state == null && password == null), or exception
+ // before having asked for the password (state != null && password
+ // == null; might also be a user cancellation), or number of
+ // attempts exhausted.
+ if (state == null || password == null
+ || state.getCount() >= attempts) {
+ return ResourceDecodeResult.TERMINATE;
+ }
+ return ResourceDecodeResult.RETRY;
+ }
+ }
+
+ @Override
+ public ResourceDecodeResult handleDecodeAttemptResult(String resourceKey,
+ String password, Exception err)
+ throws IOException, GeneralSecurityException {
+ ResourceDecodeResult result = null;
+ State state = null;
+ try {
+ state = current.get(resourceKey);
+ result = handleDecodeAttemptResult(resourceKey, state,
+ state == null ? null : state.getPassword(), err);
+ } finally {
+ if (state != null) {
+ state.setPassword(null);
+ }
+ if (result != ResourceDecodeResult.RETRY) {
+ current.remove(resourceKey);
+ }
+ }
+ return result;
+ }
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/RepeatingFilePasswordProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/RepeatingFilePasswordProvider.java
new file mode 100644
index 0000000000..da8b768441
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/RepeatingFilePasswordProvider.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf <thomas.wolf@paranor.ch>
+ * and other copyright owners as documented in the project's IP log.
+ *
+ * This program and the accompanying materials are made available
+ * under the terms of the Eclipse Distribution License v1.0 which
+ * accompanies this distribution, is reproduced below, and is
+ * available at http://www.eclipse.org/org/documents/edl-v10.php
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or
+ * without modification, are permitted provided that the following
+ * conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * - Neither the name of the Eclipse Foundation, Inc. nor the
+ * names of its contributors may be used to endorse or promote
+ * products derived from this software without specific prior
+ * written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
+ * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package org.eclipse.jgit.transport.sshd;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+
+/**
+ * A {@link FilePasswordProvider} augmented to support repeatedly asking for
+ * passwords.
+ *
+ * @since 5.2
+ */
+public interface RepeatingFilePasswordProvider extends FilePasswordProvider {
+
+ /**
+ * Define the maximum number of attempts to get a password that should be
+ * attempted for one identity resource through this provider.
+ *
+ * @param numberOfPasswordPrompts
+ * number of times to ask for a password, >= 1.
+ */
+ default void setAttempts(int numberOfPasswordPrompts) {
+ if (numberOfPasswordPrompts <= 0) {
+ throw new IllegalArgumentException(
+ "Number of password prompts must be >= 1"); //$NON-NLS-1$
+ }
+ }
+
+ /**
+ * Gets the maximum number of attempts to get a password that should be
+ * attempted for one identity resource through this provider.
+ *
+ * @return the maximum number of attempts to try, always >= 1.
+ */
+ default int getAttempts() {
+ return 1;
+ }
+
+ // The following part of this interface is from the upstream resolution of
+ // SSHD-850. See https://github.com/apache/mina-sshd/commit/f19bd2e34 .
+ // TODO: remove this once we move to sshd > 2.1.0
+
+ /**
+ * Result value of
+ * {@link RepeatingFilePasswordProvider#handleDecodeAttemptResult(String, String, Exception)}.
+ */
+ public enum ResourceDecodeResult {
+ /** Re-throw the decoding exception. */
+ TERMINATE,
+ /** Retry the decoding process - including password prompt. */
+ RETRY,
+ /** Skip attempt and see if we can proceed without the key. */
+ IGNORE;
+ }
+
+ /**
+ * Invoked to inform the password provider about the decoding result.
+ * <b>Note:</b> any exception thrown from this method (including if called
+ * to inform about success) will be propagated instead of the original (if
+ * any was reported)
+ *
+ * @param resourceKey
+ * The resource key representing the <U>private</U> file
+ * @param password
+ * The password that was attempted
+ * @param err
+ * The attempt result - {@code null} for success
+ * @return How to proceed in case of error - <u>ignored</u> if invoked in
+ * order to report success. <b>Note:</b> {@code null} is same as
+ * {@link ResourceDecodeResult#TERMINATE}.
+ * @throws IOException
+ * @throws GeneralSecurityException
+ */
+ ResourceDecodeResult handleDecodeAttemptResult(String resourceKey,
+ String password, Exception err)
+ throws IOException, GeneralSecurityException;
+}
diff --git a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java
index 3e4493119e..86dbc4edcd 100644
--- a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java
+++ b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestBase.java
@@ -54,12 +54,10 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.List;
-import java.util.Map;
import org.eclipse.jgit.api.errors.TransportException;
import org.eclipse.jgit.transport.CredentialItem;
import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.URIish;
import org.junit.Test;
import org.junit.experimental.theories.DataPoints;
import org.junit.experimental.theories.Theory;
@@ -222,6 +220,45 @@ public abstract class SshTestBase extends SshTestHarness {
}
@Test(expected = TransportException.class)
+ public void testSshEncryptedUsedKeyWrongPassword() throws Exception {
+ File encryptedKey = new File(sshDir, "id_dsa_test_key");
+ copyTestResource("id_dsa_testpass", encryptedKey);
+ File encryptedPublicKey = new File(sshDir, "id_dsa_test_key.pub");
+ copyTestResource("id_dsa_testpass.pub", encryptedPublicKey);
+ server.setTestUserPublicKey(encryptedPublicKey.toPath());
+ TestCredentialsProvider provider = new TestCredentialsProvider(
+ "wrongpass");
+ cloneWith("ssh://localhost/doesntmatter", //
+ defaultCloneDir, provider, //
+ "Host localhost", //
+ "HostName localhost", //
+ "Port " + testPort, //
+ "User " + TEST_USER, //
+ "NumberOfPasswordPrompts 1", //
+ "IdentityFile " + encryptedKey.getAbsolutePath());
+ }
+
+ @Test
+ public void testSshEncryptedUsedKeySeveralPassword() throws Exception {
+ File encryptedKey = new File(sshDir, "id_dsa_test_key");
+ copyTestResource("id_dsa_testpass", encryptedKey);
+ File encryptedPublicKey = new File(sshDir, "id_dsa_test_key.pub");
+ copyTestResource("id_dsa_testpass.pub", encryptedPublicKey);
+ server.setTestUserPublicKey(encryptedPublicKey.toPath());
+ TestCredentialsProvider provider = new TestCredentialsProvider(
+ "wrongpass", "wrongpass2", "testpass");
+ cloneWith("ssh://localhost/doesntmatter", //
+ defaultCloneDir, provider, //
+ "Host localhost", //
+ "HostName localhost", //
+ "Port " + testPort, //
+ "User " + TEST_USER, //
+ "IdentityFile " + encryptedKey.getAbsolutePath());
+ assertEquals("CredentialsProvider should have been called 3 times", 3,
+ provider.getLog().size());
+ }
+
+ @Test(expected = TransportException.class)
public void testSshWithoutKnownHosts() throws Exception {
assertTrue("Could not delete known_hosts", knownHosts.delete());
cloneWith("ssh://localhost/doesntmatter", defaultCloneDir, null, //
@@ -248,7 +285,7 @@ public abstract class SshTestBase extends SshTestHarness {
"Port " + testPort, //
"User " + TEST_USER, //
"IdentityFile " + privateKey1.getAbsolutePath());
- Map<URIish, List<CredentialItem>> messages = provider.getLog();
+ List<LogEntry> messages = provider.getLog();
assertFalse("Expected user interaction", messages.isEmpty());
if (getSessionFactory() instanceof JschConfigSessionFactory) {
// JSch doesn't create a non-existing file.
@@ -361,8 +398,8 @@ public abstract class SshTestBase extends SshTestHarness {
} catch (Exception e) {
assertEquals("Expected to be told about the modified key", 1,
provider.getLog().size());
- assertTrue("Only messages expected", provider.getLog().values()
- .stream().flatMap(List::stream).allMatch(
+ assertTrue("Only messages expected", provider.getLog().stream()
+ .flatMap(l -> l.getItems().stream()).allMatch(
c -> c instanceof CredentialItem.InformationalMessage));
throw e;
}
diff --git a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestHarness.java b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestHarness.java
index 347c72b3d7..59925a5a16 100644
--- a/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestHarness.java
+++ b/org.eclipse.jgit.test/src/org/eclipse/jgit/transport/ssh/SshTestHarness.java
@@ -56,12 +56,11 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
-import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Map;
import org.eclipse.jgit.api.CloneCommand;
import org.eclipse.jgit.api.Git;
@@ -420,15 +419,34 @@ public abstract class SshTestHarness extends RepositoryTestCase {
return true;
}
- private Map<URIish, List<CredentialItem>> log = new LinkedHashMap<>();
+ private List<LogEntry> log = new ArrayList<>();
private void logItems(URIish uri, CredentialItem... items) {
- log.put(uri, Arrays.asList(items));
+ log.add(new LogEntry(uri, Arrays.asList(items)));
}
- public Map<URIish, List<CredentialItem>> getLog() {
+ public List<LogEntry> getLog() {
return log;
}
}
+ protected static class LogEntry {
+
+ private URIish uri;
+
+ private List<CredentialItem> items;
+
+ public LogEntry(URIish uri, List<CredentialItem> items) {
+ this.uri = uri;
+ this.items = items;
+ }
+
+ public URIish getURIish() {
+ return uri;
+ }
+
+ public List<CredentialItem> getItems() {
+ return items;
+ }
+ }
}