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.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.loader;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.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.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.security;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.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}
|
||||
gssapiUnexpectedMechanism=Server {0} replied with unknown mechanism name ''{1}'' in {2} 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.
|
||||
keyEncryptedPrompt=Passphrase
|
||||
keyEncryptedRetry=Encrypted key ''{0}'' could not be decrypted. Enter the passphrase again.
|
||||
keyLoadFailed=Could not load key ''{0}''
|
||||
knownHostsCouldNotUpdate=Could not update known hosts file {0}
|
||||
knownHostsFileLockedRead=Could not read known hosts file (locked) {0}
|
||||
|
|
|
@ -56,20 +56,20 @@
|
|||
import java.util.NoSuchElementException;
|
||||
import java.util.concurrent.CancellationException;
|
||||
|
||||
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Creates a new {@link CachingKeyPairProvider} using the given
|
||||
* {@link KeyCache}. If the cache is {@code null}, this is a simple
|
||||
* {@link FileKeyPairProvider}.
|
||||
* {@link EncryptedFileKeyPairProvider}.
|
||||
*
|
||||
* @param paths
|
||||
* 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.session.ClientSessionImpl;
|
||||
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.io.IoConnectFuture;
|
||||
import org.apache.sshd.common.io.IoSession;
|
||||
|
@ -75,6 +76,7 @@
|
|||
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
|
||||
|
@ -195,6 +197,11 @@ private JGitClientSession createSession(IoSession ioSession,
|
|||
int numberOfPasswordPrompts = getNumberOfPasswordPrompts(hostConfig);
|
||||
session.getProperties().put(PASSWORD_PROMPTS,
|
||||
Integer.valueOf(numberOfPasswordPrompts));
|
||||
FilePasswordProvider provider = getFilePasswordProvider();
|
||||
if (provider instanceof RepeatingFilePasswordProvider) {
|
||||
((RepeatingFilePasswordProvider) provider)
|
||||
.setAttempts(numberOfPasswordPrompts);
|
||||
}
|
||||
FileKeyPairProvider ourConfiguredKeysProvider = null;
|
||||
List<Path> identities = hostConfig.getIdentities().stream()
|
||||
.map(s -> {
|
||||
|
|
|
@ -30,8 +30,13 @@ public static SshdText get() {
|
|||
/***/ public String gssapiInitFailure;
|
||||
/***/ public String gssapiUnexpectedMechanism;
|
||||
/***/ public String gssapiUnexpectedMessage;
|
||||
/***/ public String identityFileCannotDecrypt;
|
||||
/***/ public String identityFileNoKey;
|
||||
/***/ public String identityFileMultipleKeys;
|
||||
/***/ public String identityFileUnsupportedFormat;
|
||||
/***/ public String keyEncryptedMsg;
|
||||
/***/ public String keyEncryptedPrompt;
|
||||
/***/ public String keyEncryptedRetry;
|
||||
/***/ public String keyLoadFailed;
|
||||
/***/ public String knownHostsCouldNotUpdate;
|
||||
/***/ public String knownHostsFileLockedRead;
|
||||
|
|
|
@ -46,25 +46,98 @@
|
|||
|
||||
import java.io.IOException;
|
||||
import java.net.URISyntaxException;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
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.transport.CredentialItem;
|
||||
import org.eclipse.jgit.transport.CredentialsProvider;
|
||||
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
|
||||
*/
|
||||
public class IdentityPasswordProvider implements FilePasswordProvider {
|
||||
public class IdentityPasswordProvider implements RepeatingFilePasswordProvider {
|
||||
|
||||
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
|
||||
* an encrypted identity.
|
||||
|
@ -76,6 +149,56 @@ public IdentityPasswordProvider(CredentialsProvider 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
|
||||
* {@link CredentialsProvider} uses uris as resource identifications.
|
||||
|
@ -92,15 +215,14 @@ protected URIish toUri(String resourceKey) {
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPassword(String resourceKey) throws IOException {
|
||||
private char[] getPassword(String resourceKey, String message) {
|
||||
if (provider == null) {
|
||||
return null;
|
||||
}
|
||||
URIish file = toUri(resourceKey);
|
||||
List<CredentialItem> items = new ArrayList<>(2);
|
||||
items.add(new CredentialItem.InformationalMessage(
|
||||
format(SshdText.get().keyEncryptedMsg, resourceKey)));
|
||||
format(message, resourceKey)));
|
||||
CredentialItem.Password password = new CredentialItem.Password(
|
||||
SshdText.get().keyEncryptedPrompt);
|
||||
items.add(password);
|
||||
|
@ -111,10 +233,69 @@ public String getPassword(String resourceKey) throws IOException {
|
|||
throw new CancellationException(
|
||||
SshdText.get().authenticationCanceled);
|
||||
}
|
||||
return new String(pass);
|
||||
return pass.clone();
|
||||
} finally {
|
||||
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.file.Files;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jgit.api.errors.TransportException;
|
||||
import org.eclipse.jgit.transport.CredentialItem;
|
||||
import org.eclipse.jgit.transport.JschConfigSessionFactory;
|
||||
import org.eclipse.jgit.transport.URIish;
|
||||
import org.junit.Test;
|
||||
import org.junit.experimental.theories.DataPoints;
|
||||
import org.junit.experimental.theories.Theory;
|
||||
|
@ -221,6 +219,45 @@ public void testSshEncryptedUsedKeyCached() throws Exception {
|
|||
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)
|
||||
public void testSshWithoutKnownHosts() throws Exception {
|
||||
assertTrue("Could not delete known_hosts", knownHosts.delete());
|
||||
|
@ -248,7 +285,7 @@ public void testSshWithoutKnownHostsWithProviderAsk()
|
|||
"Port " + testPort, //
|
||||
"User " + TEST_USER, //
|
||||
"IdentityFile " + privateKey1.getAbsolutePath());
|
||||
Map<URIish, List<CredentialItem>> messages = provider.getLog();
|
||||
List<LogEntry> messages = provider.getLog();
|
||||
assertFalse("Expected user interaction", messages.isEmpty());
|
||||
if (getSessionFactory() instanceof JschConfigSessionFactory) {
|
||||
// JSch doesn't create a non-existing file.
|
||||
|
@ -361,8 +398,8 @@ public void testSshModifiedHostKeyWithProviderDeny() throws Exception {
|
|||
} catch (Exception e) {
|
||||
assertEquals("Expected to be told about the modified key", 1,
|
||||
provider.getLog().size());
|
||||
assertTrue("Only messages expected", provider.getLog().values()
|
||||
.stream().flatMap(List::stream).allMatch(
|
||||
assertTrue("Only messages expected", provider.getLog().stream()
|
||||
.flatMap(l -> l.getItems().stream()).allMatch(
|
||||
c -> c instanceof CredentialItem.InformationalMessage));
|
||||
throw e;
|
||||
}
|
||||
|
|
|
@ -56,12 +56,11 @@
|
|||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.Iterator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.eclipse.jgit.api.CloneCommand;
|
||||
import org.eclipse.jgit.api.Git;
|
||||
|
@ -420,15 +419,34 @@ public boolean get(URIish uri, CredentialItem... items)
|
|||
return true;
|
||||
}
|
||||
|
||||
private Map<URIish, List<CredentialItem>> log = new LinkedHashMap<>();
|
||||
private List<LogEntry> log = new ArrayList<>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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