]> source.dussan.org Git - jgit.git/commitdiff
Apache MINA sshd client: don't leak upstream classes and interfaces 00/132500/3
authorThomas Wolf <thomas.wolf@paranor.ch>
Thu, 15 Nov 2018 15:33:04 +0000 (16:33 +0100)
committerMatthias Sohn <matthias.sohn@sap.com>
Fri, 16 Nov 2018 23:56:31 +0000 (15:56 -0800)
We will get an API evolution problem if we expose as API classes and
interfaces that derive from upstream classes or interfaces. Upstream
interfaces also evolve quite erratically and evolution doesn't seem
to follow semantic versioning.

Introduce a new KeyPasswordProvider interface so that we don't have
to depend on the upstream FilePasswordProvider in our API. (We do
need _some_ abstraction for getting passwords for encrypted keys in
the API; EGit will need to provide its own implementation.)

Move some other upstream dependencies (HostConfigEntry, and various
previously protected methods in SshdSessionFactory) out of the API:
classes moved to internal space, and methods made private.

The only dependencies on upstream interfaces are thus in a few method
parameter types. Those cannot be avoided, but should also not pose
problems.

Bug: 520927
Change-Id: Idc9c6b0f237f29f46343c0fe15179242f2007bec
Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>
13 files changed:
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/EncryptedFileKeyPairProvider.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitHostConfigEntry.java [new file with mode: 0644]
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshClient.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java [new file with mode: 0644]
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/OpenSshServerKeyVerifier.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/PasswordProviderWrapper.java [new file with mode: 0644]
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/RepeatingFilePasswordProvider.java [new file with mode: 0644]
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/IdentityPasswordProvider.java
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/JGitHostConfigEntry.java [deleted file]
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/JGitSshConfig.java [deleted file]
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/KeyPasswordProvider.java [new file with mode: 0644]
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/RepeatingFilePasswordProvider.java [deleted file]
org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/SshdSessionFactory.java

index 2e201d882875907a4ff869308ab6b56ffbf81895..ff81989991d8df939b5b10c5b2dc755858ede963 100644 (file)
@@ -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/internal/transport/sshd/JGitHostConfigEntry.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitHostConfigEntry.java
new file mode 100644 (file)
index 0000000..8e97dad
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * 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 java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.sshd.client.config.hosts.HostConfigEntry;
+import org.eclipse.jgit.annotations.NonNull;
+
+/**
+ * A {@link HostConfigEntry} that provides access to the multi-valued keys as
+ * lists of strings. The super class treats them as single strings containing
+ * comma-separated lists.
+ *
+ * @since 5.2
+ */
+public class JGitHostConfigEntry extends HostConfigEntry {
+
+       private Map<String, List<String>> multiValuedOptions;
+
+       /**
+        * Sets the multi-valued options.
+        *
+        * @param options
+        *            to set, may be {@code null} to set an empty map
+        */
+       public void setMultiValuedOptions(Map<String, List<String>> options) {
+               multiValuedOptions = options;
+       }
+
+       /**
+        * Retrieves all multi-valued options.
+        *
+        * @return an unmodifiable map
+        */
+       @NonNull
+       public Map<String, List<String>> getMultiValuedOptions() {
+               Map<String, List<String>> options = multiValuedOptions;
+               if (options == null) {
+                       return Collections.emptyMap();
+               }
+               return Collections.unmodifiableMap(options);
+       }
+
+}
index d3289259eda17a1ce8995c1f8af56f3c195ba0d3..915b696b99ebd8901d43d2883330dffc4eb5e7ec 100644 (file)
@@ -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/internal/transport/sshd/JGitSshConfig.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/JGitSshConfig.java
new file mode 100644 (file)
index 0000000..8ca9d21
--- /dev/null
@@ -0,0 +1,141 @@
+/*
+ * 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 org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.flag;
+import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Map;
+import java.util.TreeMap;
+
+import org.apache.sshd.client.config.hosts.HostConfigEntry;
+import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.eclipse.jgit.annotations.NonNull;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
+import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry;
+import org.eclipse.jgit.transport.SshConstants;
+
+/**
+ * A {@link HostConfigEntryResolver} adapted specifically for JGit.
+ * <p>
+ * We use our own config file parser and entry resolution since the default
+ * {@link org.apache.sshd.client.config.hosts.ConfigFileHostEntryResolver
+ * ConfigFileHostEntryResolver} has a number of problems:
+ * </p>
+ * <ul>
+ * <li>It does case-insensitive pattern matching. Matching in OpenSsh is
+ * case-sensitive! Compare also bug 531118.</li>
+ * <li>It only merges values from the global items (before the first "Host"
+ * line) into the host entries. Otherwise it selects the most specific match.
+ * OpenSsh processes <em>all</em> entries in the order they appear in the file
+ * and whenever one matches, it updates values as appropriate.</li>
+ * <li>We have to ensure that ~ replacement uses the same HOME directory as
+ * JGit. Compare bug bug 526175.</li>
+ * </ul>
+ * Therefore, this re-uses the parsing and caching from
+ * {@link OpenSshConfigFile}.
+ *
+ * @since 5.2
+ */
+public class JGitSshConfig implements HostConfigEntryResolver {
+
+       private OpenSshConfigFile configFile;
+
+       /**
+        * Creates a new {@link OpenSshConfigFile} that will read the config from
+        * file {@code config} use the given file {@code home} as "home" directory.
+        *
+        * @param home
+        *            user's home directory for the purpose of ~ replacement
+        * @param config
+        *            file to load.
+        * @param localUserName
+        *            user name of the current user on the local host OS
+        */
+       public JGitSshConfig(@NonNull File home, @NonNull File config,
+                       @NonNull String localUserName) {
+               configFile = new OpenSshConfigFile(home, config, localUserName);
+       }
+
+       @Override
+       public HostConfigEntry resolveEffectiveHost(String host, int port,
+                       String username) throws IOException {
+               HostEntry entry = configFile.lookup(host, port, username);
+               JGitHostConfigEntry config = new JGitHostConfigEntry();
+               String hostName = entry.getValue(SshConstants.HOST_NAME);
+               if (hostName == null || hostName.isEmpty()) {
+                       hostName = host;
+               }
+               config.setHostName(hostName);
+               config.setHost(SshdSocketAddress.isIPv6Address(hostName) ? "" : hostName); //$NON-NLS-1$
+               String user = username != null && !username.isEmpty() ? username
+                               : entry.getValue(SshConstants.USER);
+               if (user == null || user.isEmpty()) {
+                       user = configFile.getLocalUserName();
+               }
+               config.setUsername(user);
+               int p = port >= 0 ? port : positive(entry.getValue(SshConstants.PORT));
+               config.setPort(p >= 0 ? p : SshConstants.SSH_DEFAULT_PORT);
+               config.setIdentities(entry.getValues(SshConstants.IDENTITY_FILE));
+               config.setIdentitiesOnly(
+                               flag(entry.getValue(SshConstants.IDENTITIES_ONLY)));
+               // Apache MINA conflates all keys, even multi-valued ones, in one map
+               // and puts multiple values separated by commas in one string. See
+               // the javadoc on HostConfigEntry.
+               Map<String, String> allOptions = new TreeMap<>(
+                               String.CASE_INSENSITIVE_ORDER);
+               allOptions.putAll(entry.getOptions());
+               // And what if a value contains a comma??
+               entry.getMultiValuedOptions().entrySet().stream()
+                               .forEach(e -> allOptions.put(e.getKey(),
+                                               String.join(",", e.getValue()))); //$NON-NLS-1$
+               config.setProperties(allOptions);
+               // The following is an extension from JGitHostConfigEntry
+               config.setMultiValuedOptions(entry.getMultiValuedOptions());
+               return config;
+       }
+
+}
index e511be01d5eb69edd50ac6d9abbf021e9c8ae853..540b586ddac32759a2d74444ad5df73c2ed6b2ef 100644 (file)
@@ -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<File> defaultFiles) {
+       public OpenSshServerKeyVerifier(boolean askAboutNewFile,
+                       List<Path> 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 (file)
index 0000000..93bd102
--- /dev/null
@@ -0,0 +1,137 @@
+/*
+ * 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 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<String, AtomicInteger> 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/internal/transport/sshd/RepeatingFilePasswordProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/internal/transport/sshd/RepeatingFilePasswordProvider.java
new file mode 100644 (file)
index 0000000..5d58bd6
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * 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 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;
+        *            {@link IllegalArgumentException} may be thrown if <= 0
+        */
+       void setAttempts(int numberOfPasswordPrompts);
+
+       /**
+        * 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;
+}
index 231d3f4eb3c401095d89e3c24ccdc14af4a7e5f0..2a5f2ff24923925cc2d4ee653a99266ddc573e63 100644 (file)
@@ -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<String, State> current = new HashMap<>();
+       private final Map<URIish, State> 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<CredentialItem> 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/JGitHostConfigEntry.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/JGitHostConfigEntry.java
deleted file mode 100644 (file)
index 6bffa2e..0000000
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- * 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.util.Collections;
-import java.util.List;
-import java.util.Map;
-
-import org.apache.sshd.client.config.hosts.HostConfigEntry;
-import org.eclipse.jgit.annotations.NonNull;
-
-/**
- * A {@link HostConfigEntry} that provides access to the multi-valued keys as
- * lists of strings. The super class treats them as single strings containing
- * comma-separated lists.
- *
- * @since 5.2
- */
-public class JGitHostConfigEntry extends HostConfigEntry {
-
-       private Map<String, List<String>> multiValuedOptions;
-
-       /**
-        * Sets the multi-valued options.
-        *
-        * @param options
-        *            to set, may be {@code null} to set an empty map
-        */
-       public void setMultiValuedOptions(Map<String, List<String>> options) {
-               multiValuedOptions = options;
-       }
-
-       /**
-        * Retrieves all multi-valued options.
-        *
-        * @return an unmodifiable map
-        */
-       @NonNull
-       public Map<String, List<String>> getMultiValuedOptions() {
-               Map<String, List<String>> options = multiValuedOptions;
-               if (options == null) {
-                       return Collections.emptyMap();
-               }
-               return Collections.unmodifiableMap(options);
-       }
-
-}
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/transport/sshd/JGitSshConfig.java
deleted file mode 100644 (file)
index 9638374..0000000
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * 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 static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.flag;
-import static org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.positive;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Map;
-import java.util.TreeMap;
-
-import org.apache.sshd.client.config.hosts.HostConfigEntry;
-import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
-import org.apache.sshd.common.util.net.SshdSocketAddress;
-import org.eclipse.jgit.annotations.NonNull;
-import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile;
-import org.eclipse.jgit.internal.transport.ssh.OpenSshConfigFile.HostEntry;
-import org.eclipse.jgit.transport.SshConstants;
-
-/**
- * A {@link HostConfigEntryResolver} adapted specifically for JGit.
- * <p>
- * We use our own config file parser and entry resolution since the default
- * {@link org.apache.sshd.client.config.hosts.ConfigFileHostEntryResolver
- * ConfigFileHostEntryResolver} has a number of problems:
- * </p>
- * <ul>
- * <li>It does case-insensitive pattern matching. Matching in OpenSsh is
- * case-sensitive! Compare also bug 531118.</li>
- * <li>It only merges values from the global items (before the first "Host"
- * line) into the host entries. Otherwise it selects the most specific match.
- * OpenSsh processes <em>all</em> entries in the order they appear in the file
- * and whenever one matches, it updates values as appropriate.</li>
- * <li>We have to ensure that ~ replacement uses the same HOME directory as
- * JGit. Compare bug bug 526175.</li>
- * </ul>
- * Therefore, this re-uses the parsing and caching from
- * {@link OpenSshConfigFile}.
- *
- * @since 5.2
- */
-public class JGitSshConfig implements HostConfigEntryResolver {
-
-       private OpenSshConfigFile configFile;
-
-       /**
-        * Creates a new {@link OpenSshConfigFile} that will read the config from
-        * file {@code config} use the given file {@code home} as "home" directory.
-        *
-        * @param home
-        *            user's home directory for the purpose of ~ replacement
-        * @param config
-        *            file to load.
-        * @param localUserName
-        *            user name of the current user on the local host OS
-        */
-       public JGitSshConfig(@NonNull File home, @NonNull File config,
-                       @NonNull String localUserName) {
-               configFile = new OpenSshConfigFile(home, config, localUserName);
-       }
-
-       @Override
-       public HostConfigEntry resolveEffectiveHost(String host, int port,
-                       String username) throws IOException {
-               HostEntry entry = configFile.lookup(host, port, username);
-               JGitHostConfigEntry config = new JGitHostConfigEntry();
-               String hostName = entry.getValue(SshConstants.HOST_NAME);
-               if (hostName == null || hostName.isEmpty()) {
-                       hostName = host;
-               }
-               config.setHostName(hostName);
-               config.setHost(SshdSocketAddress.isIPv6Address(hostName) ? "" : hostName); //$NON-NLS-1$
-               String user = username != null && !username.isEmpty() ? username
-                               : entry.getValue(SshConstants.USER);
-               if (user == null || user.isEmpty()) {
-                       user = configFile.getLocalUserName();
-               }
-               config.setUsername(user);
-               int p = port >= 0 ? port : positive(entry.getValue(SshConstants.PORT));
-               config.setPort(p >= 0 ? p : SshConstants.SSH_DEFAULT_PORT);
-               config.setIdentities(entry.getValues(SshConstants.IDENTITY_FILE));
-               config.setIdentitiesOnly(
-                               flag(entry.getValue(SshConstants.IDENTITIES_ONLY)));
-               // Apache MINA conflates all keys, even multi-valued ones, in one map
-               // and puts multiple values separated by commas in one string. See
-               // the javadoc on HostConfigEntry.
-               Map<String, String> allOptions = new TreeMap<>(
-                               String.CASE_INSENSITIVE_ORDER);
-               allOptions.putAll(entry.getOptions());
-               // And what if a value contains a comma??
-               entry.getMultiValuedOptions().entrySet().stream()
-                               .forEach(e -> allOptions.put(e.getKey(),
-                                               String.join(",", e.getValue()))); //$NON-NLS-1$
-               config.setProperties(allOptions);
-               // The following is an extension from JGitHostConfigEntry
-               config.setMultiValuedOptions(entry.getMultiValuedOptions());
-               return config;
-       }
-
-}
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 (file)
index 0000000..0f315a4
--- /dev/null
@@ -0,0 +1,117 @@
+/*
+ * 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.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/RepeatingFilePasswordProvider.java b/org.eclipse.jgit.ssh.apache/src/org/eclipse/jgit/transport/sshd/RepeatingFilePasswordProvider.java
deleted file mode 100644 (file)
index da8b768..0000000
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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;
-}
index 08d08090e66d938025f0f3172ddf6e001ab17121..302ba09cc857c3195a259882f125bbdaf580025b 100644 (file)
@@ -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 <em>not</em> 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.
         * </p>
@@ -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<Path> 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<NamedFactory<UserAuth>> getUserAuthFactories() {
+       private List<NamedFactory<UserAuth>> getUserAuthFactories() {
                return Collections.unmodifiableList(
                                Arrays.asList(GssApiWithMicAuthFactory.INSTANCE,
                                                JGitPublicKeyAuthFactory.INSTANCE,