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
index 2e201d882..ff8198999 100644
--- 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
@@ -63,8 +63,7 @@ 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;
+import org.eclipse.jgit.internal.transport.sshd.RepeatingFilePasswordProvider.ResourceDecodeResult;
/**
* A {@link FileKeyPairProvider} that asks repeatedly for a passphrase for an
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/JGitHostConfigEntry.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitHostConfigEntry.java
similarity index 98%
rename from org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/JGitHostConfigEntry.java
rename to org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitHostConfigEntry.java
index 6bffa2e3e..8e97dad4a 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/JGitHostConfigEntry.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitHostConfigEntry.java
@@ -40,7 +40,7 @@
* 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;
+package org.eclipse.jgit.internal.transport.sshd;
import java.util.Collections;
import java.util.List;
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 d3289259e..915b696b9 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
@@ -76,7 +76,6 @@ 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
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/JGitSshConfig.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java
similarity index 99%
rename from org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/JGitSshConfig.java
rename to org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java
index 963837425..8ca9d2103 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/JGitSshConfig.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java
@@ -40,7 +40,7 @@
* 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;
+package org.eclipse.jgit.internal.transport.sshd;
import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.flag;
import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java
index e511be01d..540b586dd 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java
@@ -86,7 +86,6 @@ 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.transport.sshd.JGitHostConfigEntry;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -181,12 +180,12 @@ public class OpenSshServerKeyVerifier
* empty or {@code null}, in which case no default files are
* installed. The files need not exist.
*/
- public OpenSshServerKeyVerifier(boolean askAboutNewFile, List defaultFiles) {
+ public OpenSshServerKeyVerifier(boolean askAboutNewFile,
+ List defaultFiles) {
if (defaultFiles != null) {
- for (File file : defaultFiles) {
- Path p = file.toPath();
- HostKeyFile newFile = new HostKeyFile(p);
- knownHostsFiles.put(p, newFile);
+ for (Path file : defaultFiles) {
+ HostKeyFile newFile = new HostKeyFile(file);
+ knownHostsFiles.put(file, newFile);
this.defaultFiles.add(newFile);
}
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
new file mode 100644
index 000000000..93bd10285
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf
+ * 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 java.io.IOException;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.transport.CredentialsProvider;
+import org.eclipse.jgit.transport.URIish;
+import org.eclipse.jgit.transport.sshd.KeyPasswordProvider;
+
+/**
+ * A bridge from sshd's {@link RepeatingFilePasswordProvider} to our
+ * {@link KeyPasswordProvider} API.
+ */
+public class PasswordProviderWrapper implements RepeatingFilePasswordProvider {
+
+ private final KeyPasswordProvider delegate;
+
+ private Map counts = new ConcurrentHashMap<>();
+
+ /**
+ * @param delegate
+ */
+ public PasswordProviderWrapper(@NonNull KeyPasswordProvider delegate) {
+ this.delegate = delegate;
+ }
+
+ @Override
+ public void setAttempts(int numberOfPasswordPrompts) {
+ delegate.setAttempts(numberOfPasswordPrompts);
+ }
+
+ @Override
+ public int getAttempts() {
+ return delegate.getAttempts();
+ }
+
+ @Override
+ public String getPassword(String resourceKey) throws IOException {
+ int attempt = counts
+ .computeIfAbsent(resourceKey, k -> new AtomicInteger()).get();
+ char[] passphrase = delegate.getPassphrase(toUri(resourceKey), attempt);
+ if (passphrase == null) {
+ return null;
+ }
+ try {
+ return new String(passphrase);
+ } finally {
+ Arrays.fill(passphrase, '\000');
+ }
+ }
+
+ @Override
+ public ResourceDecodeResult handleDecodeAttemptResult(String resourceKey,
+ String password, Exception err)
+ throws IOException, GeneralSecurityException {
+ AtomicInteger count = counts.get(resourceKey);
+ int numberOfAttempts = count == null ? 0 : count.incrementAndGet();
+ ResourceDecodeResult result = null;
+ try {
+ if (delegate.keyLoaded(toUri(resourceKey), numberOfAttempts, err)) {
+ result = ResourceDecodeResult.RETRY;
+ } else {
+ result = ResourceDecodeResult.TERMINATE;
+ }
+ } finally {
+ if (result != ResourceDecodeResult.RETRY) {
+ counts.remove(resourceKey);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Creates a {@link URIish} from a given string. The
+ * {@link CredentialsProvider} uses uris as resource identifications.
+ *
+ * @param resourceKey
+ * to convert
+ * @return the uri
+ */
+ private URIish toUri(String resourceKey) {
+ try {
+ return new URIish(resourceKey);
+ } catch (URISyntaxException e) {
+ return new URIish().setPath(resourceKey); // Doesn't check!!
+ }
+ }
+
+}
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/internal/transport/sshd/RepeatingFilePasswordProvider.java
similarity index 93%
rename from org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/RepeatingFilePasswordProvider.java
rename to org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/RepeatingFilePasswordProvider.java
index da8b76844..5d58bd6d7 100644
--- a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/RepeatingFilePasswordProvider.java
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/RepeatingFilePasswordProvider.java
@@ -40,7 +40,7 @@
* 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;
+package org.eclipse.jgit.internal.transport.sshd;
import java.io.IOException;
import java.security.GeneralSecurityException;
@@ -60,14 +60,10 @@ public interface RepeatingFilePasswordProvider extends FilePasswordProvider {
* attempted for one identity resource through this provider.
*
* @param numberOfPasswordPrompts
- * number of times to ask for a password, >= 1.
+ * number of times to ask for a password;
+ * {@link IllegalArgumentException} may be thrown if <= 0
*/
- default void setAttempts(int numberOfPasswordPrompts) {
- if (numberOfPasswordPrompts <= 0) {
- throw new IllegalArgumentException(
- "Number of password prompts must be >= 1"); //$NON-NLS-1$
- }
- }
+ void setAttempts(int numberOfPasswordPrompts);
/**
* Gets the maximum number of attempts to get a password that should be
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 231d3f4eb..2a5f2ff24 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
@@ -45,7 +45,6 @@ package org.eclipse.jgit.transport.sshd;
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;
@@ -62,12 +61,11 @@ import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.URIish;
/**
- * A {@link RepeatingFilePasswordProvider} based on a
- * {@link CredentialsProvider}.
+ * A {@link KeyPasswordProvider} based on a {@link CredentialsProvider}.
*
* @since 5.2
*/
-public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
+public class IdentityPasswordProvider implements KeyPasswordProvider {
private CredentialsProvider provider;
@@ -136,7 +134,7 @@ public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
/**
* Counts per resource key.
*/
- private final Map current = new HashMap<>();
+ private final Map current = new HashMap<>();
/**
* Creates a new {@link IdentityPasswordProvider} to get the passphrase for
@@ -151,8 +149,10 @@ public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
@Override
public void setAttempts(int numberOfPasswordPrompts) {
- RepeatingFilePasswordProvider.super.setAttempts(
- numberOfPasswordPrompts);
+ if (numberOfPasswordPrompts <= 0) {
+ throw new IllegalArgumentException(
+ "Number of password prompts must be >= 1"); //$NON-NLS-1$
+ }
attempts = numberOfPasswordPrompts;
}
@@ -162,24 +162,18 @@ public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
}
@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');
- }
+ public char[] getPassphrase(URIish uri, int attempt) throws IOException {
+ return getPassword(uri, attempt,
+ current.computeIfAbsent(uri, r -> new State()));
}
/**
* Retrieves a password to decrypt a private key.
*
- * @param resourceKey
+ * @param uri
* identifying the resource to obtain a password for
+ * @param attempt
+ * number of previous attempts to get a passphrase
* @param state
* encapsulating state information about attempts to get the
* password
@@ -188,46 +182,29 @@ public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
* @throws IOException
* if an error occurs
*/
- protected char[] getPassword(String resourceKey, @NonNull State state)
+ protected char[] getPassword(URIish uri, int attempt, @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);
+ char[] pass = getPassword(uri, message);
state.setPassword(pass);
return pass;
}
- /**
- * Creates a {@link URIish} from a given string. The
- * {@link CredentialsProvider} uses uris as resource identifications.
- *
- * @param resourceKey
- * to convert
- * @return the uri
- */
- protected URIish toUri(String resourceKey) {
- try {
- return new URIish(resourceKey);
- } catch (URISyntaxException e) {
- return new URIish().setPath(resourceKey); // Doesn't check!!
- }
- }
-
- private char[] getPassword(String resourceKey, String message) {
+ private char[] getPassword(URIish uri, String message) {
if (provider == null) {
return null;
}
- URIish file = toUri(resourceKey);
List items = new ArrayList<>(2);
items.add(new CredentialItem.InformationalMessage(
- format(message, resourceKey)));
+ format(message, uri)));
CredentialItem.Password password = new CredentialItem.Password(
SshdText.get().keyEncryptedPrompt);
items.add(password);
try {
- provider.get(file, items);
+ provider.get(uri, items);
char[] pass = password.getValue();
if (pass == null) {
throw new CancellationException(
@@ -242,8 +219,9 @@ public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
/**
* Invoked to inform the password provider about the decoding result.
*
- * @param resourceKey
- * the resource key
+ * @param uri
+ * identifying the key resource the key was attempted to be
+ * loaded from
* @param state
* associated with this key
* @param password
@@ -253,18 +231,15 @@ public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
* @return how to proceed in case of error
* @throws IOException
* @throws GeneralSecurityException
- * @see #handleDecodeAttemptResult(String, String, Exception)
*/
- protected ResourceDecodeResult handleDecodeAttemptResult(String resourceKey,
+ protected boolean keyLoaded(URIish uri,
State state, char[] password, Exception err)
throws IOException, GeneralSecurityException {
if (err == null) {
- return null;
+ return false; // Success, don't retry
} else if (err instanceof GeneralSecurityException) {
throw new InvalidKeyException(
- format(SshdText.get().identityFileCannotDecrypt,
- resourceKey),
- err);
+ format(SshdText.get().identityFileCannotDecrypt, uri), err);
} else {
// Unencrypted key (state == null && password == null), or exception
// before having asked for the password (state != null && password
@@ -272,30 +247,29 @@ public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
// attempts exhausted.
if (state == null || password == null
|| state.getCount() >= attempts) {
- return ResourceDecodeResult.TERMINATE;
+ return false;
}
- return ResourceDecodeResult.RETRY;
+ return true;
}
}
@Override
- public ResourceDecodeResult handleDecodeAttemptResult(String resourceKey,
- String password, Exception err)
+ public boolean keyLoaded(URIish uri, int attempt, Exception error)
throws IOException, GeneralSecurityException {
- ResourceDecodeResult result = null;
State state = null;
+ boolean retry = false;
try {
- state = current.get(resourceKey);
- result = handleDecodeAttemptResult(resourceKey, state,
- state == null ? null : state.getPassword(), err);
+ state = current.get(uri);
+ retry = keyLoaded(uri, state,
+ state == null ? null : state.getPassword(), error);
} finally {
if (state != null) {
state.setPassword(null);
}
- if (result != ResourceDecodeResult.RETRY) {
- current.remove(resourceKey);
+ if (!retry) {
+ current.remove(uri);
}
}
- return result;
+ return retry;
}
}
diff --git a/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProvider.java
new file mode 100644
index 000000000..0f315a454
--- /dev/null
+++ b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProvider.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2018, Thomas Wolf
+ * 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.eclipse.jgit.transport.URIish;
+
+/**
+ * A {@code KeyPasswordProvider} provides passwords for encrypted private keys.
+ *
+ * @since 5.2
+ */
+public interface KeyPasswordProvider {
+
+ /**
+ * Obtains a passphrase to use to decrypt an ecrypted private key. Returning
+ * {@code null} or an empty array will skip this key. To cancel completely,
+ * the operation should raise
+ * {@link java.util.concurrent.CancellationException}.
+ *
+ * @param uri
+ * identifying the key resource that is being attempted to be
+ * loaded
+ * @param attempt
+ * the number of previous attempts to get a passphrase; >= 0
+ * @return the passphrase
+ * @throws IOException
+ * if no password can be obtained
+ */
+ char[] getPassphrase(URIish uri, int attempt) throws IOException;
+
+ /**
+ * Define the maximum number of attempts to get a passphrase that should be
+ * attempted for one identity resource through this provider.
+ *
+ * @param maxNumberOfAttempts
+ * number of times to ask for a passphrase;
+ * {@link IllegalArgumentException} may be thrown if <= 0
+ */
+ void setAttempts(int maxNumberOfAttempts);
+
+ /**
+ * Gets the maximum number of attempts to get a passphrase that should be
+ * attempted for one identity resource through this provider. The default
+ * return 1.
+ *
+ * @return the number of times to ask for a passphrase; should be >= 1.
+ */
+ default int getAttempts() {
+ return 1;
+ }
+
+ /**
+ * Invoked after a key has been loaded. If this raises an exception, the
+ * original {@code error} is lost unless it is attached to that exception.
+ *
+ * @param uri
+ * identifying the key resource the key was attempted to be
+ * loaded from
+ * @param attempt
+ * the number of times {@link #getPassphrase(URIish, int)} had
+ * been called; zero indicates that {@code uri} refers to a
+ * non-encrypted key
+ * @param error
+ * {@code null} if the key was loaded successfully; otherwise an
+ * exception indicating why the key could not be loaded
+ * @return {@code true} to re-try again; {@code false} to re-raise the
+ * {@code error} exception; Ignored if the key was loaded
+ * successfully, i.e., if {@code error == null}.
+ * @throws IOException
+ * @throws GeneralSecurityException
+ */
+ boolean keyLoaded(URIish uri, int attempt, Exception error)
+ throws IOException, GeneralSecurityException;
+}
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 08d08090e..302ba09cc 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
@@ -77,8 +77,10 @@ import org.eclipse.jgit.internal.transport.sshd.CachingKeyPairProvider;
import org.eclipse.jgit.internal.transport.sshd.GssApiWithMicAuthFactory;
import org.eclipse.jgit.internal.transport.sshd.JGitPublicKeyAuthFactory;
import org.eclipse.jgit.internal.transport.sshd.JGitSshClient;
+import org.eclipse.jgit.internal.transport.sshd.JGitSshConfig;
import org.eclipse.jgit.internal.transport.sshd.JGitUserInteraction;
import org.eclipse.jgit.internal.transport.sshd.OpenSshServerKeyVerifier;
+import org.eclipse.jgit.internal.transport.sshd.PasswordProviderWrapper;
import org.eclipse.jgit.internal.transport.sshd.SshdText;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.SshConstants;
@@ -127,8 +129,8 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
* {@link KeyCache} is still the right choice, for instance to avoid that a
* user gets prompted several times for the same password for the same key.
* In general, however, it is preferable not to use a key cache but
- * to use a {@link #createFilePasswordProvider(CredentialsProvider)
- * FilePasswordProvider} that has access to some secure storage and can save
+ * to use a {@link #createKeyPasswordProvider(CredentialsProvider)
+ * KeyPasswordProvider} that has access to some secure storage and can save
* and retrieve passwords from there without user interaction. Another
* approach is to use an ssh agent.
*
@@ -201,10 +203,12 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
home, sshDir);
KeyPairProvider defaultKeysProvider = getDefaultKeysProvider(
sshDir);
+ KeyPasswordProvider passphrases = createKeyPasswordProvider(
+ credentialsProvider);
SshClient client = ClientBuilder.builder()
.factory(JGitSshClient::new)
.filePasswordProvider(
- createFilePasswordProvider(credentialsProvider))
+ createFilePasswordProvider(passphrases))
.hostConfigEntryResolver(configFile)
.serverKeyVerifier(getServerKeyVerifier(home, sshDir))
.compressionFactories(
@@ -335,7 +339,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
* @return the resolver
*/
@NonNull
- protected HostConfigEntryResolver getHostConfigEntryResolver(
+ private HostConfigEntryResolver getHostConfigEntryResolver(
@NonNull File homeDir, @NonNull File sshDir) {
return defaultHostConfigEntryResolver.computeIfAbsent(
new Tuple(homeDir, sshDir),
@@ -359,15 +363,26 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
* @return the resolver
*/
@NonNull
- protected ServerKeyVerifier getServerKeyVerifier(@NonNull File homeDir,
+ private ServerKeyVerifier getServerKeyVerifier(@NonNull File homeDir,
@NonNull File sshDir) {
return defaultServerKeyVerifier.computeIfAbsent(
new Tuple(homeDir, sshDir),
t -> new OpenSshServerKeyVerifier(true,
- Arrays.asList(
- new File(sshDir, SshConstants.KNOWN_HOSTS),
- new File(sshDir,
- SshConstants.KNOWN_HOSTS + '2'))));
+ getDefaultKnownHostsFiles(sshDir)));
+ }
+
+ /**
+ * Gets the list of default user known hosts files. The default returns
+ * ~/.ssh/known_hosts and ~/.ssh/known_hosts2. The ssh config
+ * {@code UserKnownHostsFile} overrides this default.
+ *
+ * @param sshDir
+ * @return the possibly empty list of default known host file paths.
+ */
+ @NonNull
+ protected List getDefaultKnownHostsFiles(@NonNull File sshDir) {
+ return Arrays.asList(sshDir.toPath().resolve(SshConstants.KNOWN_HOSTS),
+ sshDir.toPath().resolve(SshConstants.KNOWN_HOSTS + '2'));
}
/**
@@ -378,7 +393,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
* @return the {@link KeyPairProvider}
*/
@NonNull
- protected KeyPairProvider getDefaultKeysProvider(@NonNull File sshDir) {
+ private KeyPairProvider getDefaultKeysProvider(@NonNull File sshDir) {
return defaultKeys.computeIfAbsent(new Tuple(sshDir),
t -> new CachingKeyPairProvider(getDefaultIdentities(sshDir),
getKeyCache()));
@@ -413,19 +428,32 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
}
/**
- * Creates a {@link FilePasswordProvider} for a new session.
+ * Creates a {@link KeyPasswordProvider} for a new session.
*
* @param provider
- * the {@link CredentialsProvider} to delegate for for user
+ * the {@link CredentialsProvider} to delegate to for user
* interactions
- * @return a new {@link FilePasswordProvider}
+ * @return a new {@link KeyPasswordProvider}
*/
@NonNull
- protected FilePasswordProvider createFilePasswordProvider(
+ protected KeyPasswordProvider createKeyPasswordProvider(
CredentialsProvider provider) {
return new IdentityPasswordProvider(provider);
}
+ /**
+ * Creates a {@link FilePasswordProvider} for a new session.
+ *
+ * @param provider
+ * the {@link KeyPasswordProvider} to delegate to
+ * @return a new {@link FilePasswordProvider}
+ */
+ @NonNull
+ private FilePasswordProvider createFilePasswordProvider(
+ KeyPasswordProvider provider) {
+ return new PasswordProviderWrapper(provider);
+ }
+
/**
* Gets the user authentication mechanisms (or rather, factories for them).
* By default this returns gssapi-with-mic, public-key,
@@ -437,7 +465,7 @@ public class SshdSessionFactory extends SshSessionFactory implements Closeable {
* @return the non-empty list of factories.
*/
@NonNull
- protected List> getUserAuthFactories() {
+ private List> getUserAuthFactories() {
return Collections.unmodifiableList(
Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
JGitPublicKeyAuthFactory.INSTANCE,