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