Provide a mechanism for a subclass to provide its own set of default identities from anywhere as an Iterable<KeyPair>. The default implementation is functionally unchanged and uses the known default identity files in the ~/.ssh directory. A subclass can override the getDefaultKeys() function and return whatever keys are appropriate. Bug: 543152 Change-Id: I500d63146bc67e20e051f617790eb87c7cb500b6 Signed-off-by: Thomas Wolf <thomas.wolf@paranor.ch>tags/v5.3.0.201903061415-rc1
* A {@link EncryptedFileKeyPairProvider} that uses an external | * A {@link EncryptedFileKeyPairProvider} that uses an external | ||||
* {@link KeyCache}. | * {@link KeyCache}. | ||||
*/ | */ | ||||
public class CachingKeyPairProvider extends EncryptedFileKeyPairProvider { | |||||
public class CachingKeyPairProvider extends EncryptedFileKeyPairProvider | |||||
implements Iterable<KeyPair> { | |||||
private final KeyCache cache; | private final KeyCache cache; | ||||
} | } | ||||
@Override | @Override | ||||
protected Iterable<KeyPair> loadKeys(Collection<? extends Path> resources) { | |||||
public Iterator<KeyPair> iterator() { | |||||
Collection<? extends Path> resources = getPaths(); | |||||
if (resources.isEmpty()) { | if (resources.isEmpty()) { | ||||
return Collections.emptyList(); | |||||
return Collections.emptyListIterator(); | |||||
} | } | ||||
return () -> new CancellingKeyPairIterator(resources); | |||||
return new CancellingKeyPairIterator(resources); | |||||
} | |||||
@Override | |||||
public Iterable<KeyPair> loadKeys() { | |||||
return this; | |||||
} | } | ||||
@Override | @Override |
* encrypted private key if the {@link FilePasswordProvider} is a | * encrypted private key if the {@link FilePasswordProvider} is a | ||||
* {@link RepeatingFilePasswordProvider}. | * {@link RepeatingFilePasswordProvider}. | ||||
*/ | */ | ||||
public class EncryptedFileKeyPairProvider extends FileKeyPairProvider { | |||||
public abstract class EncryptedFileKeyPairProvider extends FileKeyPairProvider { | |||||
// TODO: remove this class once we're based on sshd > 2.1.0. See upstream | // TODO: remove this class once we're based on sshd > 2.1.0. See upstream | ||||
// issue SSHD-850 https://issues.apache.org/jira/browse/SSHD-850 and commit | // issue SSHD-850 https://issues.apache.org/jira/browse/SSHD-850 and commit |
import org.apache.sshd.common.future.SshFutureListener; | import org.apache.sshd.common.future.SshFutureListener; | ||||
import org.apache.sshd.common.io.IoConnectFuture; | import org.apache.sshd.common.io.IoConnectFuture; | ||||
import org.apache.sshd.common.io.IoSession; | import org.apache.sshd.common.io.IoSession; | ||||
import org.apache.sshd.common.keyprovider.FileKeyPairProvider; | |||||
import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider; | |||||
import org.apache.sshd.common.keyprovider.KeyPairProvider; | import org.apache.sshd.common.keyprovider.KeyPairProvider; | ||||
import org.apache.sshd.common.session.helpers.AbstractSession; | import org.apache.sshd.common.session.helpers.AbstractSession; | ||||
import org.apache.sshd.common.util.ValidateUtils; | import org.apache.sshd.common.util.ValidateUtils; | ||||
int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig); | int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig); | ||||
session.getProperties().put(PASSWORD_PROMPTS, | session.getProperties().put(PASSWORD_PROMPTS, | ||||
Integer.valueOf(numberOfPasswordPrompts)); | Integer.valueOf(numberOfPasswordPrompts)); | ||||
FilePasswordProvider provider = getFilePasswordProvider(); | |||||
if (provider instanceof RepeatingFilePasswordProvider) { | |||||
((RepeatingFilePasswordProvider) provider) | |||||
FilePasswordProvider passwordProvider = getFilePasswordProvider(); | |||||
if (passwordProvider instanceof RepeatingFilePasswordProvider) { | |||||
((RepeatingFilePasswordProvider) passwordProvider) | |||||
.setAttempts(numberOfPasswordPrompts); | .setAttempts(numberOfPasswordPrompts); | ||||
} | } | ||||
FileKeyPairProvider ourConfiguredKeysProvider = null; | |||||
List<Path> identities = hostConfig.getIdentities().stream() | List<Path> identities = hostConfig.getIdentities().stream() | ||||
.map(s -> { | .map(s -> { | ||||
try { | try { | ||||
} | } | ||||
}).filter(p -> p != null && Files.exists(p)) | }).filter(p -> p != null && Files.exists(p)) | ||||
.collect(Collectors.toList()); | .collect(Collectors.toList()); | ||||
ourConfiguredKeysProvider = new CachingKeyPairProvider(identities, | |||||
keyCache); | |||||
ourConfiguredKeysProvider.setPasswordFinder(getFilePasswordProvider()); | |||||
CachingKeyPairProvider ourConfiguredKeysProvider = new CachingKeyPairProvider( | |||||
identities, keyCache); | |||||
ourConfiguredKeysProvider.setPasswordFinder(passwordProvider); | |||||
if (hostConfig.isIdentitiesOnly()) { | if (hostConfig.isIdentitiesOnly()) { | ||||
session.setKeyPairProvider(ourConfiguredKeysProvider); | session.setKeyPairProvider(ourConfiguredKeysProvider); | ||||
} else { | } else { | ||||
KeyPairProvider defaultKeysProvider = getKeyPairProvider(); | KeyPairProvider defaultKeysProvider = getKeyPairProvider(); | ||||
if (defaultKeysProvider instanceof FileKeyPairProvider) { | |||||
((FileKeyPairProvider) defaultKeysProvider) | |||||
.setPasswordFinder(getFilePasswordProvider()); | |||||
if (defaultKeysProvider instanceof AbstractResourceKeyPairProvider<?>) { | |||||
((AbstractResourceKeyPairProvider<?>) defaultKeysProvider) | |||||
.setPasswordFinder(passwordProvider); | |||||
} | } | ||||
KeyPairProvider combinedProvider = new CombinedKeyPairProvider( | KeyPairProvider combinedProvider = new CombinedKeyPairProvider( | ||||
ourConfiguredKeysProvider, defaultKeysProvider); | ourConfiguredKeysProvider, defaultKeysProvider); |
import java.io.IOException; | import java.io.IOException; | ||||
import java.nio.file.Files; | import java.nio.file.Files; | ||||
import java.nio.file.Path; | import java.nio.file.Path; | ||||
import java.security.KeyPair; | |||||
import java.time.Duration; | import java.time.Duration; | ||||
import java.util.ArrayList; | import java.util.ArrayList; | ||||
import java.util.Arrays; | import java.util.Arrays; | ||||
import org.apache.sshd.common.NamedFactory; | import org.apache.sshd.common.NamedFactory; | ||||
import org.apache.sshd.common.compression.BuiltinCompressions; | import org.apache.sshd.common.compression.BuiltinCompressions; | ||||
import org.apache.sshd.common.config.keys.FilePasswordProvider; | import org.apache.sshd.common.config.keys.FilePasswordProvider; | ||||
import org.apache.sshd.common.keyprovider.FileKeyPairProvider; | |||||
import org.apache.sshd.common.keyprovider.KeyPairProvider; | import org.apache.sshd.common.keyprovider.KeyPairProvider; | ||||
import org.eclipse.jgit.annotations.NonNull; | import org.eclipse.jgit.annotations.NonNull; | ||||
import org.eclipse.jgit.errors.TransportException; | import org.eclipse.jgit.errors.TransportException; | ||||
import org.eclipse.jgit.util.FS; | import org.eclipse.jgit.util.FS; | ||||
/** | /** | ||||
* A {@link SshSessionFactory} that uses Apache MINA sshd. | |||||
* A {@link SshSessionFactory} that uses Apache MINA sshd. Classes from Apache | |||||
* MINA sshd are kept private to avoid API evolution problems when Apache MINA | |||||
* sshd interfaces change. | |||||
* | * | ||||
* @since 5.2 | * @since 5.2 | ||||
*/ | */ | ||||
private final Map<Tuple, ServerKeyVerifier> defaultServerKeyVerifier = new ConcurrentHashMap<>(); | private final Map<Tuple, ServerKeyVerifier> defaultServerKeyVerifier = new ConcurrentHashMap<>(); | ||||
private final Map<Tuple, FileKeyPairProvider> defaultKeys = new ConcurrentHashMap<>(); | |||||
private final Map<Tuple, Iterable<KeyPair>> defaultKeys = new ConcurrentHashMap<>(); | |||||
private final KeyCache keyCache; | private final KeyCache keyCache; | ||||
} | } | ||||
HostConfigEntryResolver configFile = getHostConfigEntryResolver( | HostConfigEntryResolver configFile = getHostConfigEntryResolver( | ||||
home, sshDir); | home, sshDir); | ||||
KeyPairProvider defaultKeysProvider = getDefaultKeysProvider( | |||||
sshDir); | |||||
KeyPairProvider defaultKeysProvider = toKeyPairProvider( | |||||
getDefaultKeys(sshDir)); | |||||
KeyPasswordProvider passphrases = createKeyPasswordProvider( | KeyPasswordProvider passphrases = createKeyPasswordProvider( | ||||
credentialsProvider); | credentialsProvider); | ||||
SshClient client = ClientBuilder.builder() | SshClient client = ClientBuilder.builder() | ||||
} | } | ||||
/** | /** | ||||
* Determines a {@link KeyPairProvider} to use to load the default keys. | |||||
* Determines the default keys. The default implementation will lazy load | |||||
* the {@link #getDefaultIdentities(File) default identity files}. | |||||
* <p> | |||||
* Subclasses may override and return an {@link Iterable} of whatever keys | |||||
* are appropriate. If the returned iterable lazily loads keys, it should be | |||||
* an instance of | |||||
* {@link org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider | |||||
* AbstractResourceKeyPairProvider} so that the session can later pass it | |||||
* the {@link #createKeyPasswordProvider(CredentialsProvider) password | |||||
* provider} wrapped as a {@link FilePasswordProvider} via | |||||
* {@link org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider#setPasswordFinder(FilePasswordProvider) | |||||
* AbstractResourceKeyPairProvider#setPasswordFinder(FilePasswordProvider)} | |||||
* so that encrypted, password-protected keys can be loaded. | |||||
* </p> | |||||
* <p> | |||||
* The default implementation uses exactly this mechanism; class | |||||
* {@link CachingKeyPairProvider} may serve as a model for a customized | |||||
* lazy-loading {@link Iterable} implementation | |||||
* </p> | |||||
* <p> | |||||
* If the {@link Iterable} returned has the keys already pre-loaded or | |||||
* otherwise doesn't need to decrypt encrypted keys, it can be any | |||||
* {@link Iterable}, for instance a simple {@link java.util.List List}. | |||||
* </p> | |||||
* | * | ||||
* @param sshDir | * @param sshDir | ||||
* to look in for keys | * to look in for keys | ||||
* @return the {@link KeyPairProvider} | |||||
* @return an {@link Iterable} over the default keys | |||||
* @since 5.3 | |||||
*/ | */ | ||||
@NonNull | @NonNull | ||||
private KeyPairProvider getDefaultKeysProvider(@NonNull File sshDir) { | |||||
protected Iterable<KeyPair> getDefaultKeys(@NonNull File sshDir) { | |||||
List<Path> defaultIdentities = getDefaultIdentities(sshDir); | List<Path> defaultIdentities = getDefaultIdentities(sshDir); | ||||
return defaultKeys.computeIfAbsent( | return defaultKeys.computeIfAbsent( | ||||
new Tuple(defaultIdentities.toArray(new Path[0])), | new Tuple(defaultIdentities.toArray(new Path[0])), | ||||
getKeyCache())); | getKeyCache())); | ||||
} | } | ||||
/** | |||||
* Converts an {@link Iterable} of {link KeyPair}s into a | |||||
* {@link KeyPairProvider}. | |||||
* | |||||
* @param keys | |||||
* to provide via the returned {@link KeyPairProvider} | |||||
* @return a {@link KeyPairProvider} that provides the given {@code keys} | |||||
*/ | |||||
private KeyPairProvider toKeyPairProvider(Iterable<KeyPair> keys) { | |||||
if (keys instanceof KeyPairProvider) { | |||||
return (KeyPairProvider) keys; | |||||
} | |||||
return () -> keys; | |||||
} | |||||
/** | /** | ||||
* Gets a list of default identities, i.e., private key files that shall | * Gets a list of default identities, i.e., private key files that shall | ||||
* always be tried for public key authentication. Typically those are | * always be tried for public key authentication. Typically those are |