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
--- /dev/null
+/*
+ * 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);
+ }
+
+}
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
--- /dev/null
+/*
+ * 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;
+ }
+
+}
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;
* 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);
}
}
--- /dev/null
+/*
+ * 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!!
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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;
+}
import static java.text.MessageFormat.format;
import java.io.IOException;
-import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.security.InvalidKeyException;
import java.util.ArrayList;
import 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;
/**
* 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
@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;
}
}
@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
* @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(
/**
* 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
* @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
// 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;
}
}
+++ /dev/null
-/*
- * 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);
- }
-
-}
+++ /dev/null
-/*
- * 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;
- }
-
-}
--- /dev/null
+/*
+ * 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;
+}
+++ /dev/null
-/*
- * 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;
-}
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;
* {@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>
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(
* @return the resolver
*/
@NonNull
- protected HostConfigEntryResolver getHostConfigEntryResolver(
+ private HostConfigEntryResolver getHostConfigEntryResolver(
@NonNull File homeDir, @NonNull File sshDir) {
return defaultHostConfigEntryResolver.computeIfAbsent(
new Tuple(homeDir, sshDir),
* @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'));
}
/**
* @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()));
}
/**
- * 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,
* @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,