Discussion:
mina-sshd git commit: WIP
l***@apache.org
2018-10-19 04:14:10 UTC
Permalink
Repository: mina-sshd
Updated Branches:
refs/heads/SSHD-850 [created] 673337432


WIP


Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/67333743
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/67333743
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/67333743

Branch: refs/heads/SSHD-850
Commit: 673337432cb675e2def40f6c0315da0e3f71e110
Parents: 326725d
Author: Lyor Goldstein <***@apache.org>
Authored: Fri Oct 19 07:01:40 2018 +0300
Committer: Lyor Goldstein <***@apache.org>
Committed: Fri Oct 19 07:19:33 2018 +0300

----------------------------------------------------------------------
README.md | 26 +++--
.../config/keys/FilePasswordProvider.java | 32 +++++-
.../common/config/keys/KeyEntryResolver.java | 30 ++++--
.../config/keys/PrivateKeyEntryDecoder.java | 3 +-
.../config/keys/PublicKeyEntryDecoder.java | 3 +-
.../keys/impl/ECDSAPublicKeyEntryDecoder.java | 7 +-
.../keys/loader/KeyPairResourceLoader.java | 6 ++
.../OpenSSHECDSAPrivateKeyEntryDecoder.java | 5 +-
.../openssh/OpenSSHKeyPairResourceParser.java | 21 ++--
.../pem/AbstractPEMResourceKeyPairParser.java | 50 ++++++---
.../BouncyCastleKeyPairResourceParser.java | 49 +++++++--
.../security/eddsa/Ed25519PublicKeyDecoder.java | 4 +-
.../OpenSSHEd25519PrivateKeyEntryDecoder.java | 4 +-
.../common/util/security/SecurityUtilsTest.java | 35 ++++--
.../util/test/CountingPasswordProvider.java | 106 +++++++++++++++++++
.../loader/putty/AbstractPuttyKeyDecoder.java | 47 ++++++--
.../keys/loader/putty/ECDSAPuttyKeyDecoder.java | 2 +-
.../keys/loader/putty/EdDSAPuttyKeyDecoder.java | 4 +-
.../keys/loader/putty/PuttyKeyReader.java | 12 ++-
.../keys/loader/putty/PuttyKeyUtilsTest.java | 33 +++++-
20 files changed, 403 insertions(+), 76 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index bfbc5cb..28d3ec9 100644
--- a/README.md
+++ b/README.md
@@ -216,16 +216,30 @@ Of course, one can implement the verifier in whatever other manner is suitable f

### ClientIdentityLoader/KeyPairProvider

-One can set up the public/private keys to be used in case a password-less authentication is needed. By default, the client is configured to automatically detect and use the identity files residing in the user's *~/.ssh* folder (e.g., *id_rsa*, *id_ecdsa*) and present them as part of the authentication process. **Note:** if the identity files are encrypted via a password, one must configure a `FilePasswordProvider` so that the code can decrypt them before using and presenting them to the server as part of the authentication process. Reading key files in PEM format (including encrypted ones) is supported by default for the standard keys and formats. Using additional non-standard special features requires that the [Bouncy Castle](https://www.bouncycastle.org/) supporting artifacts be available in the code's classpath. One can also read files in
-[OpenSSH](http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?rev=1.1&content-type=text/x-cvsweb-markup)
-format without any specific extra artifacts (although for reading _ed25519_ keys one needs to add the _EdDSA_ support
-artifacts). **Note:** for the time being, password encrypted _ed25519_ private key files are not supported.
+One can set up the public/private keys to be used in case a password-less authentication is needed. By default, the client is configured to automatically
+detect and use the identity files residing in the user's *~/.ssh* folder (e.g., *id_rsa*, *id_ecdsa*) and present them as part of the authentication process.
+**Note:** if the identity files are encrypted via a password, one must configure a `FilePasswordProvider` so that the code can decrypt them before using
+and presenting them to the server as part of the authentication process. Reading key files in PEM format (including encrypted ones) is supported by default
+for the standard keys and formats. Using additional non-standard special features requires that the [Bouncy Castle](https://www.bouncycastle.org/) supporting
+artifacts be available in the code's classpath. One can also read files in
+[OpenSSH](http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?rev=1.1&content-type=text/x-cvsweb-markup) format without any specific extra
+artifacts (although for reading _ed25519_ keys one needs to add the _EdDSA_ support artifacts). **Note:** for the time being, password
+encrypted _ed25519_ private key files are not supported.
+
+The `FilePasswordProvider` has support for a retry mechanism via its `handleDecodeAttemptResult`. When the code detects an encrypted private key,
+it will start a loop where it prompts for the password, attempts to decode the key using the provided password and then informs the provider of
+the outcome - success or failure. If failure is signaled, then provider can decide whether to retry using a new password, abort (with exception)
+or ignore. If provider chooses to ignore the failure, then the code will make a best effort to proceed without the key.

### UserInteraction

-This interface is required for full support of `keyboard-interactive` authentication protocol as described in [RFC 4256](https://www.ietf.org/rfc/rfc4256.txt). The client can handle a simple password request from the server, but if more complex challenge-response interaction is required, then this interface must be provided - including support for `SSH_MSG_USERAUTH_PASSWD_CHANGEREQ` as described in [RFC 4252 section 8](https://www.ietf.org/rfc/rfc4252.txt).
+This interface is required for full support of `keyboard-interactive` authentication protocol as described in [RFC 4256](https://www.ietf.org/rfc/rfc4256.txt).
+The client can handle a simple password request from the server, but if more complex challenge-response interaction is required, then this interface must be
+provided - including support for `SSH_MSG_USERAUTH_PASSWD_CHANGEREQ` as described in [RFC 4252 section 8](https://www.ietf.org/rfc/rfc4252.txt).

-While RFC-4256 support is the primary purpose of this interface, it can also be used to retrieve the server's welcome banner as described in [RFC 4252 section 5.4](https://www.ietf.org/rfc/rfc4252.txt) as well as its initial identification string as described in [RFC 4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2).
+While RFC-4256 support is the primary purpose of this interface, it can also be used to retrieve the server's welcome banner as described
+in [RFC 4252 section 5.4](https://www.ietf.org/rfc/rfc4252.txt) as well as its initial identification string as described
+in [RFC 4253 section 4.2](https://tools.ietf.org/html/rfc4253#section-4.2).

## Using the `SshClient` to connect to a server


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
index 064f75c..ad434ba 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/FilePasswordProvider.java
@@ -20,25 +20,53 @@
package org.apache.sshd.common.config.keys;

import java.io.IOException;
+import java.security.GeneralSecurityException;

/**
* @author <a href="mailto:***@mina.apache.org">Apache MINA SSHD Project</a>
*/
@FunctionalInterface
public interface FilePasswordProvider {
+ enum ResourceDecodeResult {
+ /** Re-throw the decoding exception */
+ TERMINATE,
+ /** Try again the decoding process - including password prompt */
+ RETRY,
+ /** Skip attempt and see if can proceed without the key */
+ IGNORE;
+ }
+
/**
* An &quot;empty&quot; provider that returns {@code null} - i.e., unprotected key file
*/
FilePasswordProvider EMPTY = resourceKey -> null;

/**
- * @param resourceKey The resource key representing the <U>private</U>
- * file
+ * @param resourceKey The resource key representing the <U>private</U> file
* @return The password - if {@code null}/empty then no password is required
* @throws IOException if cannot resolve password
*/
String getPassword(String resourceKey) throws IOException;

+ /**
+ * Invoked to inform the password provide 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 If cannot resolve a new password
+ * @throws GeneralSecurityException If not attempting to resolve a new password
+ */
+ default ResourceDecodeResult handleDecodeAttemptResult(
+ String resourceKey, String password, Exception err)
+ throws IOException, GeneralSecurityException {
+ return ResourceDecodeResult.TERMINATE;
+ }
+
static FilePasswordProvider of(String password) {
return r -> password;
}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java
index 4bfbea0..5c272c3 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/KeyEntryResolver.java
@@ -22,6 +22,7 @@ package org.apache.sshd.common.config.keys;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import java.io.StreamCorruptedException;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@@ -42,6 +43,12 @@ import org.apache.sshd.common.util.io.IoUtils;
*/
public interface KeyEntryResolver<PUB extends PublicKey, PRV extends PrivateKey> extends IdentityResourceLoader<PUB, PRV> {
/**
+ * A reasonable max. number of octets used for a {@link BigInteger} int the
+ * context of keys based on such numbers
+ */
+ int MAX_BIGINT_OCTETS_COUNT = Short.MAX_VALUE;
+
+ /**
* @param keySize Key size in bits
* @return A {@link KeyPair} with the specified key size
* @throws GeneralSecurityException if unable to generate the pair
@@ -155,25 +162,32 @@ public interface KeyEntryResolver<PUB extends PublicKey, PRV extends PrivateKey>
return bytes;
}

- static String decodeString(InputStream s) throws IOException {
- return decodeString(s, StandardCharsets.UTF_8);
+ static String decodeString(InputStream s, int maxChars) throws IOException {
+ return decodeString(s, StandardCharsets.UTF_8, maxChars);
}

- static String decodeString(InputStream s, String charset) throws IOException {
- return decodeString(s, Charset.forName(charset));
+ static String decodeString(InputStream s, String charset, int maxChars) throws IOException {
+ return decodeString(s, Charset.forName(charset), maxChars);
}

- static String decodeString(InputStream s, Charset cs) throws IOException {
- byte[] bytes = readRLEBytes(s);
+ static String decodeString(InputStream s, Charset cs, int maxChars) throws IOException {
+ byte[] bytes = readRLEBytes(s, maxChars * 4 /* in case UTF-8 with weird characters */);
return new String(bytes, cs);
}

static BigInteger decodeBigInt(InputStream s) throws IOException {
- return new BigInteger(readRLEBytes(s));
+ return new BigInteger(readRLEBytes(s, MAX_BIGINT_OCTETS_COUNT));
}

- static byte[] readRLEBytes(InputStream s) throws IOException {
+ static byte[] readRLEBytes(InputStream s, int maxAllowed) throws IOException {
int len = decodeInt(s);
+ if (len > maxAllowed) {
+ throw new StreamCorruptedException("Requested block length (" + len + ") exceeds max. allowed (" + maxAllowed + ")");
+ }
+ if (len < 0) {
+ throw new StreamCorruptedException("Negative block length requested: " + len);
+ }
+
byte[] bytes = new byte[len];
IoUtils.readFully(s, bytes);
return bytes;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java
index 6dbeee9..8dc6113 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PrivateKeyEntryDecoder.java
@@ -31,6 +31,7 @@ import java.security.spec.InvalidKeySpecException;
import java.util.Collection;
import java.util.Objects;

+import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.ValidateUtils;
@@ -83,7 +84,7 @@ public interface PrivateKeyEntryDecoder<PUB extends PublicKey, PRV extends Priva
default PRV decodePrivateKey(FilePasswordProvider passwordProvider, InputStream keyData)
throws IOException, GeneralSecurityException {
// the actual data is preceded by a string that repeats the key type
- String type = KeyEntryResolver.decodeString(keyData);
+ String type = KeyEntryResolver.decodeString(keyData, KeyPairResourceLoader.MAX_KEY_TYPE_NAME_LENGTH);
if (GenericUtils.isEmpty(type)) {
throw new StreamCorruptedException("Missing key type string");
}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java
index 7462e4a..ad42781 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/PublicKeyEntryDecoder.java
@@ -30,6 +30,7 @@ import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Collection;

+import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.NumberUtils;
import org.apache.sshd.common.util.ValidateUtils;
@@ -78,7 +79,7 @@ public interface PublicKeyEntryDecoder<PUB extends PublicKey, PRV extends Privat

default PUB decodePublicKey(InputStream keyData) throws IOException, GeneralSecurityException {
// the actual data is preceded by a string that repeats the key type
- String type = KeyEntryResolver.decodeString(keyData);
+ String type = KeyEntryResolver.decodeString(keyData, KeyPairResourceLoader.MAX_KEY_TYPE_NAME_LENGTH);
if (GenericUtils.isEmpty(type)) {
throw new StreamCorruptedException("Missing key type string");
}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java
index 397a007..07f3970 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java
@@ -47,6 +47,9 @@ import org.apache.sshd.common.util.security.SecurityUtils;
* @author <a href="mailto:***@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<ECPublicKey, ECPrivateKey> {
+ public static final int MAX_ALLOWED_POINT_SIZE = KeyEntryResolver.MAX_BIGINT_OCTETS_COUNT;
+ public static final int MAX_CURVE_NAME_LENGTH = 1024;
+
public static final ECDSAPublicKeyEntryDecoder INSTANCE = new ECDSAPublicKeyEntryDecoder();

// see rfc5480 section 2.2
@@ -71,12 +74,12 @@ public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<EC

String keyCurveName = curve.getName();
// see rfc5656 section 3.1
- String encCurveName = KeyEntryResolver.decodeString(keyData);
+ String encCurveName = KeyEntryResolver.decodeString(keyData, MAX_CURVE_NAME_LENGTH);
if (!keyCurveName.equals(encCurveName)) {
throw new InvalidKeySpecException("Mismatched key curve name (" + keyCurveName + ") vs. encoded one (" + encCurveName + ")");
}

- byte[] octets = KeyEntryResolver.readRLEBytes(keyData);
+ byte[] octets = KeyEntryResolver.readRLEBytes(keyData, MAX_ALLOWED_POINT_SIZE);
ECPoint w;
try {
w = ECCurves.octetStringToEcPoint(octets);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java
index fa6930a..659870f 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java
@@ -48,6 +48,12 @@ import org.apache.sshd.common.util.io.IoUtils;
*/
@FunctionalInterface
public interface KeyPairResourceLoader {
+ int MAX_CIPHER_NAME_LENGTH = 256;
+ int MAX_KEY_TYPE_NAME_LENGTH = 256;
+ int MAX_KEY_COMMENT_LENGTH = 1024;
+ int MAX_PUBLIC_KEY_DATA_SIZE = 2 * Short.MAX_VALUE;
+ int MAX_PRIVATE_KEY_DATA_SIZE = 4 * MAX_PUBLIC_KEY_DATA_SIZE;
+
/**
* An empty loader that never fails but always returns an empty list
*/

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java
index bceca59..83a0fb3 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java
@@ -42,6 +42,7 @@ import org.apache.sshd.common.config.keys.FilePasswordProvider;
import org.apache.sshd.common.config.keys.KeyEntryResolver;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.impl.AbstractPrivateKeyEntryDecoder;
+import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder;
import org.apache.sshd.common.util.security.SecurityUtils;

/**
@@ -68,12 +69,12 @@ public class OpenSSHECDSAPrivateKeyEntryDecoder extends AbstractPrivateKeyEntryD

String keyCurveName = curve.getName();
// see rfc5656 section 3.1
- String encCurveName = KeyEntryResolver.decodeString(keyData);
+ String encCurveName = KeyEntryResolver.decodeString(keyData, ECDSAPublicKeyEntryDecoder.MAX_CURVE_NAME_LENGTH);
if (!keyCurveName.equals(encCurveName)) {
throw new InvalidKeySpecException("Mismatched key curve name (" + keyCurveName + ") vs. encoded one (" + encCurveName + ")");
}

- byte[] pubKey = KeyEntryResolver.readRLEBytes(keyData);
+ byte[] pubKey = KeyEntryResolver.readRLEBytes(keyData, ECDSAPublicKeyEntryDecoder.MAX_ALLOWED_POINT_SIZE);
Objects.requireNonNull(pubKey, "No public point"); // TODO validate it is a valid ECPoint
BigInteger s = KeyEntryResolver.decodeBigInt(keyData);
ECParameterSpec params = curve.getParameters();

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java
index 95db256..9dc2dd8 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java
@@ -59,6 +59,9 @@ import org.apache.sshd.common.util.security.SecurityUtils;
* @author <a href="mailto:***@mina.apache.org">Apache MINA SSHD Project</a>
*/
public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser {
+ public static final int MAX_KDF_NAME_LENGTH = 1024;
+ public static final int MAX_KDF_OPTIONS_SIZE = Short.MAX_VALUE;
+
public static final String BEGIN_MARKER = "BEGIN OPENSSH PRIVATE KEY";
public static final List<String> BEGINNERS =
Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER));
@@ -96,10 +99,10 @@ public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser
@Override
public Collection<KeyPair> extractKeyPairs(
String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, InputStream stream)
- throws IOException, GeneralSecurityException {
+ throws IOException, GeneralSecurityException {
stream = validateStreamMagicMarker(resourceKey, stream);

- String cipher = KeyEntryResolver.decodeString(stream);
+ String cipher = KeyEntryResolver.decodeString(stream, MAX_CIPHER_NAME_LENGTH);
if (!OpenSSHParserContext.IS_NONE_CIPHER.test(cipher)) {
throw new NoSuchAlgorithmException("Unsupported cipher: " + cipher);
}
@@ -109,12 +112,12 @@ public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser
log.debug("extractKeyPairs({}) cipher={}", resourceKey, cipher);
}

- String kdfName = KeyEntryResolver.decodeString(stream);
+ String kdfName = KeyEntryResolver.decodeString(stream, MAX_KDF_NAME_LENGTH);
if (!OpenSSHParserContext.IS_NONE_KDF.test(kdfName)) {
throw new NoSuchAlgorithmException("Unsupported KDF: " + kdfName);
}

- byte[] kdfOptions = KeyEntryResolver.readRLEBytes(stream);
+ byte[] kdfOptions = KeyEntryResolver.readRLEBytes(stream, MAX_KDF_OPTIONS_SIZE);
if (debugEnabled) {
log.debug("extractKeyPairs({}) KDF={}, options={}",
resourceKey, kdfName, BufferUtils.toHex(':', kdfOptions));
@@ -141,7 +144,7 @@ public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser
publicKeys.add(pubKey);
}

- byte[] privateData = KeyEntryResolver.readRLEBytes(stream);
+ byte[] privateData = KeyEntryResolver.readRLEBytes(stream, MAX_PRIVATE_KEY_DATA_SIZE);
try (InputStream bais = new ByteArrayInputStream(privateData)) {
return readPrivateKeys(resourceKey, context, publicKeys, passwordProvider, bais);
}
@@ -150,9 +153,9 @@ public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser
protected PublicKey readPublicKey(
String resourceKey, OpenSSHParserContext context, InputStream stream)
throws IOException, GeneralSecurityException {
- byte[] keyData = KeyEntryResolver.readRLEBytes(stream);
+ byte[] keyData = KeyEntryResolver.readRLEBytes(stream, MAX_PUBLIC_KEY_DATA_SIZE);
try (InputStream bais = new ByteArrayInputStream(keyData)) {
- String keyType = KeyEntryResolver.decodeString(bais);
+ String keyType = KeyEntryResolver.decodeString(bais, MAX_KEY_TYPE_NAME_LENGTH);
PublicKeyEntryDecoder<?, ?> decoder = KeyUtils.getPublicKeyEntryDecoder(keyType);
if (decoder == null) {
throw new NoSuchAlgorithmException("Unsupported key type (" + keyType + ") in " + resourceKey);
@@ -209,7 +212,7 @@ public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser
protected SimpleImmutableEntry<PrivateKey, String> readPrivateKey(
String resourceKey, OpenSSHParserContext context, String keyType, FilePasswordProvider passwordProvider, InputStream stream)
throws IOException, GeneralSecurityException {
- String prvType = KeyEntryResolver.decodeString(stream);
+ String prvType = KeyEntryResolver.decodeString(stream, MAX_KEY_TYPE_NAME_LENGTH);
if (!Objects.equals(keyType, prvType)) {
throw new StreamCorruptedException("Mismatched private key type: "
+ ", expected=" + keyType + ", actual=" + prvType
@@ -226,7 +229,7 @@ public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser
throw new InvalidKeyException("Cannot parse key type (" + prvType + ") in " + resourceKey);
}

- String comment = KeyEntryResolver.decodeString(stream);
+ String comment = KeyEntryResolver.decodeString(stream, MAX_KEY_COMMENT_LENGTH);
return new SimpleImmutableEntry<>(prvKey, comment);
}


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java
index bee13d6..6778610 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/pem/AbstractPEMResourceKeyPairParser.java
@@ -22,6 +22,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StreamCorruptedException;
+import java.net.ProtocolException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
@@ -33,6 +34,7 @@ import javax.security.auth.login.CredentialException;
import javax.security.auth.login.FailedLoginException;

import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
import org.apache.sshd.common.config.keys.loader.AbstractKeyPairResourceParser;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
import org.apache.sshd.common.config.keys.loader.PrivateKeyEncryptionContext;
@@ -71,7 +73,7 @@ public abstract class AbstractPEMResourceKeyPairParser
@Override
public Collection<KeyPair> extractKeyPairs(
String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, List<String> lines)
- throws IOException, GeneralSecurityException {
+ throws IOException, GeneralSecurityException {
if (GenericUtils.isEmpty(lines)) {
return Collections.emptyList();
}
@@ -129,18 +131,42 @@ public abstract class AbstractPEMResourceKeyPairParser
throw new CredentialException("Missing password provider for encrypted resource=" + resourceKey);
}

- String password = passwordProvider.getPassword(resourceKey);
- if (GenericUtils.isEmpty(password)) {
- throw new FailedLoginException("No password data for encrypted resource=" + resourceKey);
- }
+ while (true) {
+ String password = passwordProvider.getPassword(resourceKey);
+ Collection<KeyPair> keys;
+ try {
+ if (GenericUtils.isEmpty(password)) {
+ throw new FailedLoginException("No password data for encrypted resource=" + resourceKey);
+ }
+
+ PrivateKeyEncryptionContext encContext = new PrivateKeyEncryptionContext(algInfo);
+ encContext.setPassword(password);
+ encContext.setInitVector(initVector);
+ byte[] encryptedData = KeyPairResourceParser.extractDataBytes(dataLines);
+ byte[] decodedData = applyPrivateKeyCipher(encryptedData, encContext, false);
+ try (InputStream bais = new ByteArrayInputStream(decodedData)) {
+ keys = extractKeyPairs(resourceKey, beginMarker, endMarker, passwordProvider, bais);
+ }
+ } catch (IOException | GeneralSecurityException | RuntimeException e) {
+ ResourceDecodeResult result =
+ passwordProvider.handleDecodeAttemptResult(resourceKey, password, e);
+ if (result == null) {
+ result = ResourceDecodeResult.TERMINATE;
+ }
+ switch (result) {
+ case TERMINATE:
+ throw e;
+ case RETRY:
+ continue;
+ case IGNORE:
+ return Collections.emptyList();
+ default:
+ throw new ProtocolException("Unsupported decode attempt result (" + result + ") for " + resourceKey);
+ }
+ }

- PrivateKeyEncryptionContext encContext = new PrivateKeyEncryptionContext(algInfo);
- encContext.setPassword(password);
- encContext.setInitVector(initVector);
- byte[] encryptedData = KeyPairResourceParser.extractDataBytes(dataLines);
- byte[] decodedData = applyPrivateKeyCipher(encryptedData, encContext, false);
- try (InputStream bais = new ByteArrayInputStream(decodedData)) {
- return extractKeyPairs(resourceKey, beginMarker, endMarker, passwordProvider, bais);
+ passwordProvider.handleDecodeAttemptResult(resourceKey, password, null);
+ return keys;
}
}


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java
index 4c8722a..5d07e15 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/bouncycastle/BouncyCastleKeyPairResourceParser.java
@@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
+import java.net.ProtocolException;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
@@ -32,9 +33,13 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;

+import javax.security.auth.login.CredentialException;
+import javax.security.auth.login.FailedLoginException;
+
import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
import org.apache.sshd.common.config.keys.loader.AbstractKeyPairResourceParser;
-import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.security.SecurityProviderRegistrar;
import org.apache.sshd.common.util.security.SecurityUtils;
@@ -109,13 +114,45 @@ public class BouncyCastleKeyPairResourceParser extends AbstractKeyPairResourcePa
} else {
pemConverter.setProvider(registrar.getSecurityProvider());
}
+
if (o instanceof PEMEncryptedKeyPair) {
- ValidateUtils.checkNotNull(provider, "No password provider for resource=%s", resourceKey);
+ if (provider == null) {
+ throw new CredentialException("Missing password provider for encrypted resource=" + resourceKey);
+ }
+
+ while (true) {
+ String password = provider.getPassword(resourceKey);
+ PEMKeyPair decoded;
+ try {
+ if (GenericUtils.isEmpty(password)) {
+ throw new FailedLoginException("No password data for encrypted resource=" + resourceKey);
+ }
+
+ JcePEMDecryptorProviderBuilder decryptorBuilder = new JcePEMDecryptorProviderBuilder();
+ PEMDecryptorProvider pemDecryptor = decryptorBuilder.build(password.toCharArray());
+ decoded = ((PEMEncryptedKeyPair) o).decryptKeyPair(pemDecryptor);
+ } catch (IOException | GeneralSecurityException | RuntimeException e) {
+ ResourceDecodeResult result =
+ provider.handleDecodeAttemptResult(resourceKey, password, e);
+ if (result == null) {
+ result = ResourceDecodeResult.TERMINATE;
+ }
+ switch (result) {
+ case TERMINATE:
+ throw e;
+ case RETRY:
+ continue;
+ case IGNORE:
+ return null;
+ default:
+ throw new ProtocolException("Unsupported decode attempt result (" + result + ") for " + resourceKey);
+ }
+ }

- String password = ValidateUtils.checkNotNullAndNotEmpty(provider.getPassword(resourceKey), "No password provided for resource=%s", resourceKey);
- JcePEMDecryptorProviderBuilder decryptorBuilder = new JcePEMDecryptorProviderBuilder();
- PEMDecryptorProvider pemDecryptor = decryptorBuilder.build(password.toCharArray());
- o = ((PEMEncryptedKeyPair) o).decryptKeyPair(pemDecryptor);
+ o = decoded;
+ provider.handleDecodeAttemptResult(resourceKey, password, null);
+ break;
+ }
}

if (o instanceof PEMKeyPair) {

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java
index 3366510..aa75171 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/Ed25519PublicKeyDecoder.java
@@ -41,6 +41,8 @@ import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec;
* @author <a href="mailto:***@mina.apache.org">Apache MINA SSHD Project</a>
*/
public final class Ed25519PublicKeyDecoder extends AbstractPublicKeyEntryDecoder<EdDSAPublicKey, EdDSAPrivateKey> {
+ public static final int MAX_ALLOWED_SEED_LEN = 1024; // in reality it is much less than this
+
public static final Ed25519PublicKeyDecoder INSTANCE = new Ed25519PublicKeyDecoder();

private Ed25519PublicKeyDecoder() {
@@ -87,7 +89,7 @@ public final class Ed25519PublicKeyDecoder extends AbstractPublicKeyEntryDecoder

@Override
public EdDSAPublicKey decodePublicKey(String keyType, InputStream keyData) throws IOException, GeneralSecurityException {
- byte[] seed = KeyEntryResolver.readRLEBytes(keyData);
+ byte[] seed = KeyEntryResolver.readRLEBytes(keyData, MAX_ALLOWED_SEED_LEN);
return EdDSAPublicKey.class.cast(SecurityUtils.generateEDDSAPublicKey(keyType, seed));
}


http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java
index d707d47..f0fdfbb 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/security/eddsa/OpenSSHEd25519PrivateKeyEntryDecoder.java
@@ -74,8 +74,8 @@ public class OpenSSHEd25519PrivateKeyEntryDecoder extends AbstractPrivateKeyEntr
// we expect to find two byte arrays with the following structure (type:size):
// [pk:32], [sk:32,pk:32]

- byte[] pk = KeyEntryResolver.readRLEBytes(keyData);
- byte[] keypair = KeyEntryResolver.readRLEBytes(keyData);
+ byte[] pk = KeyEntryResolver.readRLEBytes(keyData, PK_SIZE * 2);
+ byte[] keypair = KeyEntryResolver.readRLEBytes(keyData, KEYPAIR_SIZE * 2);

if (pk.length != PK_SIZE) {
throw new InvalidKeyException(String.format(Locale.ENGLISH, "Unexpected pk size: %s (expected %s)", pk.length, PK_SIZE));

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/test/java/org/apache/sshd/common/util/security/SecurityUtilsTest.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/test/java/org/apache/sshd/common/util/security/SecurityUtilsTest.java b/sshd-common/src/test/java/org/apache/sshd/common/util/security/SecurityUtilsTest.java
index 46ca075..50a0f08 100644
--- a/sshd-common/src/test/java/org/apache/sshd/common/util/security/SecurityUtilsTest.java
+++ b/sshd-common/src/test/java/org/apache/sshd/common/util/security/SecurityUtilsTest.java
@@ -33,17 +33,19 @@ import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;
import java.util.Collection;
+import java.util.EnumSet;
import java.util.List;

import org.apache.sshd.common.cipher.BuiltinCiphers;
import org.apache.sshd.common.cipher.ECCurves;
-import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceLoader;
import org.apache.sshd.common.keyprovider.AbstractResourceKeyPairProvider;
import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider;
import org.apache.sshd.common.keyprovider.FileKeyPairProvider;
import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
import org.apache.sshd.util.test.JUnitTestSupport;
import org.apache.sshd.util.test.NoIoTestCase;
import org.junit.AfterClass;
@@ -52,12 +54,18 @@ import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;

/**
* @author <a href="mailto:***@mina.apache.org">Apache MINA SSHD Project</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
@Category({ NoIoTestCase.class })
@SuppressWarnings("checkstyle:MethodCount")
public class SecurityUtilsTest extends JUnitTestSupport {
@@ -67,10 +75,16 @@ public class SecurityUtilsTest extends JUnitTestSupport {
+ "." + SecurityProviderRegistrar.NAMED_PROVIDER_PROPERTY;

private static final String DEFAULT_PASSWORD = "super secret passphrase";
- private static final FilePasswordProvider TEST_PASSWORD_PROVIDER = file -> DEFAULT_PASSWORD;

- public SecurityUtilsTest() {
- super();
+ private final ResourceDecodeResult result;
+
+ public SecurityUtilsTest(ResourceDecodeResult result) {
+ this.result = result;
+ }
+
+ @Parameters(name = "{0}")
+ public static List<Object[]> parameters() {
+ return parameterize(EnumSet.allOf(ResourceDecodeResult.class));
}

// NOTE: Using the BouncyCastle provider instead of the name does not work as expected so we take no chances
@@ -167,15 +181,15 @@ public class SecurityUtilsTest extends JUnitTestSupport {
return kpResource;
}

- private static KeyPair testLoadPrivateKeyResource(String name, Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType) {
+ private KeyPair testLoadPrivateKeyResource(String name, Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType) {
return testLoadPrivateKey(name, new ClassLoadableResourceKeyPairProvider(name), pubType, prvType);
}

- private static KeyPair testLoadPrivateKeyFile(Path file, Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType) {
+ private KeyPair testLoadPrivateKeyFile(Path file, Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType) {
return testLoadPrivateKey(file.toString(), new FileKeyPairProvider(file), pubType, prvType);
}

- private static KeyPair testLoadPrivateKey(String resourceKey, AbstractResourceKeyPairProvider<?> provider,
+ private KeyPair testLoadPrivateKey(String resourceKey, AbstractResourceKeyPairProvider<?> provider,
Class<? extends PublicKey> pubType, Class<? extends PrivateKey> prvType) {
provider.setPasswordFinder(TEST_PASSWORD_PROVIDER);
Iterable<KeyPair> iterator = provider.loadKeys();
@@ -200,6 +214,7 @@ public class SecurityUtilsTest extends JUnitTestSupport {

@Test
public void testSetMaxDHGroupExchangeKeySizeByProperty() {
+ Assume.assumeTrue("Skip duplicate tests", result == ResourceDecodeResult.IGNORE);
try {
for (int expected = SecurityUtils.MIN_DHGEX_KEY_SIZE; expected <= SecurityUtils.MAX_DHGEX_KEY_SIZE; expected += 1024) {
SecurityUtils.setMaxDHGroupExchangeKeySize(0); // force detection
@@ -218,6 +233,7 @@ public class SecurityUtilsTest extends JUnitTestSupport {

@Test
public void testSetMaxDHGroupExchangeKeySizeProgrammatically() {
+ Assume.assumeTrue("Skip duplicate tests", result == ResourceDecodeResult.IGNORE);
try {
for (int expected = SecurityUtils.MIN_DHGEX_KEY_SIZE; expected <= SecurityUtils.MAX_DHGEX_KEY_SIZE; expected += 1024) {
SecurityUtils.setMaxDHGroupExchangeKeySize(expected);
@@ -228,4 +244,9 @@ public class SecurityUtilsTest extends JUnitTestSupport {
SecurityUtils.setMaxDHGroupExchangeKeySize(0); // force detection
}
}
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[" + result + "]";
+ }
}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-common/src/test/java/org/apache/sshd/util/test/CountingPasswordProvider.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/test/java/org/apache/sshd/util/test/CountingPasswordProvider.java b/sshd-common/src/test/java/org/apache/sshd/util/test/CountingPasswordProvider.java
new file mode 100644
index 0000000..39b340e
--- /dev/null
+++ b/sshd-common/src/test/java/org/apache/sshd/util/test/CountingPasswordProvider.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.util.test;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+
+/**
+ * @author <a href="mailto:***@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class CountingPasswordProvider implements FilePasswordProvider {
+ private final ResourceDecodeResult result;
+ private final int maxRetries;
+ private final AtomicInteger retriesCount = new AtomicInteger(0);
+ private final String password;
+
+ public CountingPasswordProvider(String password, ResourceDecodeResult result, int maxRetries) {
+ this.password = password;
+ this.result = result;
+ this.maxRetries = maxRetries;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public ResourceDecodeResult getResult() {
+ return result;
+ }
+
+ public int getMaxRetries() {
+ return maxRetries;
+ }
+
+ public int getRetriesCount() {
+ return retriesCount.get();
+ }
+
+ @Override
+ public String getPassword(String resourceKey) throws IOException {
+ ResourceDecodeResult r = getResult();
+ switch (r) {
+ case IGNORE:
+ case TERMINATE:
+ return "qwertyuiop[]{}0123456789(!@#$%^-*)" + System.nanoTime();
+ case RETRY: {
+ int count = retriesCount.incrementAndGet();
+ if (count == getMaxRetries()) {
+ return getPassword();
+ } else {
+ return "retry #" + count;
+ }
+ }
+ default:
+ throw new UnsupportedOperationException("Unknown decode result type: " + r);
+ }
+ }
+
+ @Override
+ public ResourceDecodeResult handleDecodeAttemptResult(
+ String resourceKey, String password, Exception err)
+ throws IOException, GeneralSecurityException {
+ if (err == null) {
+ return null;
+ }
+
+ ResourceDecodeResult r = getResult();
+ if (r == ResourceDecodeResult.RETRY) {
+ if (getRetriesCount() >= getMaxRetries()) {
+ return ResourceDecodeResult.TERMINATE;
+ }
+ }
+
+ return r;
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName()
+ + "[password=" + password
+ + ", result=" + getResult()
+ + ", maxRetries=" + getMaxRetries()
+ + ", retriesCount=" + getRetriesCount()
+ + "]";
+ }
+}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/AbstractPuttyKeyDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/AbstractPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/AbstractPuttyKeyDecoder.java
index d2428e2..681422a 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/AbstractPuttyKeyDecoder.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/AbstractPuttyKeyDecoder.java
@@ -23,6 +23,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.StreamCorruptedException;
+import java.net.ProtocolException;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PrivateKey;
@@ -33,7 +34,10 @@ import java.util.Collection;
import java.util.Collections;
import java.util.List;

+import javax.security.auth.login.FailedLoginException;
+
import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
import org.apache.sshd.common.config.keys.impl.AbstractIdentityResourceLoader;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
import org.apache.sshd.common.util.GenericUtils;
@@ -155,15 +159,8 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
Decoder b64Decoder = Base64.getDecoder();
byte[] pubBytes = b64Decoder.decode(pubData);
byte[] prvBytes = b64Decoder.decode(prvData);
- String password = null;
- if ((GenericUtils.length(prvEncryption) > 0)
- && (!NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption))) {
- password = passwordProvider.getPassword(resourceKey);
- }
-
if (GenericUtils.isEmpty(prvEncryption)
- || NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption)
- || GenericUtils.isEmpty(password)) {
+ || NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption)) {
return loadKeyPairs(resourceKey, pubBytes, prvBytes);
}

@@ -189,8 +186,38 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
throw new StreamCorruptedException("Missing private key encryption algorithm details in " + prvEncryption);
}

- prvBytes = PuttyKeyPairResourceParser.decodePrivateKeyBytes(prvBytes, algName, numBits, mode, password);
- return loadKeyPairs(resourceKey, pubBytes, prvBytes);
+ while (true) {
+ String password = passwordProvider.getPassword(resourceKey);
+
+ Collection<KeyPair> keys;
+ try {
+ if (GenericUtils.isEmpty(password)) {
+ throw new FailedLoginException("No password data for encrypted resource=" + resourceKey);
+ }
+
+ byte[] decBytes = PuttyKeyPairResourceParser.decodePrivateKeyBytes(prvBytes, algName, numBits, mode, password);
+ keys = loadKeyPairs(resourceKey, pubBytes, decBytes);
+ } catch (IOException | GeneralSecurityException | RuntimeException e) {
+ ResourceDecodeResult result =
+ passwordProvider.handleDecodeAttemptResult(resourceKey, password, e);
+ if (result == null) {
+ result = ResourceDecodeResult.TERMINATE;
+ }
+ switch (result) {
+ case TERMINATE:
+ throw e;
+ case RETRY:
+ continue;
+ case IGNORE:
+ return Collections.emptyList();
+ default:
+ throw new ProtocolException("Unsupported decode attempt result (" + result + ") for " + resourceKey);
+ }
+ }
+
+ passwordProvider.handleDecodeAttemptResult(resourceKey, password, null);
+ return keys;
+ }
}

public Collection<KeyPair> loadKeyPairs(String resourceKey, byte[] pubData, byte[] prvData)

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/ECDSAPuttyKeyDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/ECDSAPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/ECDSAPuttyKeyDecoder.java
index a257ff8..f5860f1 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/ECDSAPuttyKeyDecoder.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/ECDSAPuttyKeyDecoder.java
@@ -72,7 +72,7 @@ public class ECDSAPuttyKeyDecoder extends AbstractPuttyKeyDecoder<ECPublicKey, E
throw new InvalidKeySpecException("Mismatched key curve name (" + keyCurveName + ") vs. encoded one (" + encCurveName + ")");
}

- byte[] octets = pubReader.read();
+ byte[] octets = pubReader.read(Short.MAX_VALUE); // reasonable max. allowed size
ECPoint w;
try {
w = ECCurves.octetStringToEcPoint(octets);

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/EdDSAPuttyKeyDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/EdDSAPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/EdDSAPuttyKeyDecoder.java
index f5980ab..f610983 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/EdDSAPuttyKeyDecoder.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/EdDSAPuttyKeyDecoder.java
@@ -59,9 +59,9 @@ public class EdDSAPuttyKeyDecoder extends AbstractPuttyKeyDecoder<EdDSAPublicKey
throw new InvalidKeySpecException("Not an " + SecurityUtils.EDDSA + " key: " + keyType);
}

- byte[] seed = pubReader.read();
+ byte[] seed = pubReader.read(Short.MAX_VALUE); // reasonable max. allowed size
PublicKey pubKey = EdDSASecurityProviderUtils.generateEDDSAPublicKey(seed);
- seed = prvReader.read();
+ seed = prvReader.read(Short.MAX_VALUE); // reasonable max. allowed size
PrivateKey prvKey = EdDSASecurityProviderUtils.generateEDDSAPrivateKey(seed);
return Collections.singletonList(new KeyPair(pubKey, prvKey));
}

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyReader.java
----------------------------------------------------------------------
diff --git a/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyReader.java b/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyReader.java
index 4fb63d1..07dd97b 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyReader.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyReader.java
@@ -52,17 +52,23 @@ public class PuttyKeyReader implements Closeable {
}

public String readString(Charset cs) throws IOException {
- byte[] data = read();
+ byte[] data = read(Short.MAX_VALUE); // reasonable value for any expected string
return new String(data, cs);
}

public BigInteger readInt() throws IOException {
- byte[] bytes = read();
+ byte[] bytes = read(Short.MAX_VALUE); // reasonable value for any expected BigInteger
return new BigInteger(bytes);
}

- public byte[] read() throws IOException {
+ public byte[] read(int maxAllowed) throws IOException {
int len = di.readInt();
+ if (len > maxAllowed) {
+ throw new StreamCorruptedException("Requested block length (" + len + ") exceeds max. allowed (" + maxAllowed + ")");
+ }
+ if (len < 0) {
+ throw new StreamCorruptedException("Negative block length requested: " + len);
+ }
byte[] r = new byte[len];
di.readFully(r);
return r;

http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/67333743/sshd-putty/src/test/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyUtilsTest.java
----------------------------------------------------------------------
diff --git a/sshd-putty/src/test/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyUtilsTest.java b/sshd-putty/src/test/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyUtilsTest.java
index b9f030f..d597101 100644
--- a/sshd-putty/src/test/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyUtilsTest.java
+++ b/sshd-putty/src/test/java/org/apache/sshd/common/config/keys/loader/putty/PuttyKeyUtilsTest.java
@@ -29,11 +29,13 @@ import java.util.Collection;
import java.util.List;

import org.apache.sshd.common.cipher.BuiltinCiphers;
+import org.apache.sshd.common.config.keys.FilePasswordProvider.ResourceDecodeResult;
import org.apache.sshd.common.config.keys.KeyUtils;
import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder;
import org.apache.sshd.common.config.keys.loader.openssh.OpenSSHKeyPairResourceParser;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.io.IoUtils;
+import org.apache.sshd.util.test.CountingPasswordProvider;
import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
import org.apache.sshd.util.test.JUnitTestSupport;
import org.apache.sshd.util.test.NoIoTestCase;
@@ -100,7 +102,7 @@ public class PuttyKeyUtilsTest extends JUnitTestSupport {
}

assertFalse(other.getClass().getSimpleName() + "/" + resource + " - unexpected extraction capability",
- other.canExtractKeyPairs(resource, lines));
+ other.canExtractKeyPairs(resource, lines));
}
}
}
@@ -128,6 +130,35 @@ public class PuttyKeyUtilsTest extends JUnitTestSupport {
assertLoadedKeyPair(encryptedFile, keys.iterator().next());
}

+ @Test
+ public void testDecodeEncryptedFileWithRetries() throws IOException, GeneralSecurityException {
+ Assume.assumeTrue(BuiltinCiphers.aes256cbc.getTransformation() + " N/A", BuiltinCiphers.aes256cbc.isSupported());
+ URL url = getClass().getResource(encryptedFile);
+ Assume.assumeTrue("Skip non-existent encrypted file: " + encryptedFile, url != null);
+ assertNotNull("Missing test resource: " + encryptedFile, url);
+
+ for (ResourceDecodeResult result : ResourceDecodeResult.values()) {
+ CountingPasswordProvider provider = new CountingPasswordProvider(PASSWORD, result, 3);
+ try {
+ Collection<KeyPair> keys = parser.loadKeyPairs(url, provider);
+ if (result == ResourceDecodeResult.IGNORE) {
+ assertEquals("Unexpected loaded keys count from " + encryptedFile, 0, GenericUtils.size(keys));
+ assertEquals("Mismatched " + result + " retries count", 0, provider.getRetriesCount());
+ } else {
+ assertEquals("Mismatched loaded keys count from " + encryptedFile, 1, GenericUtils.size(keys));
+ assertEquals("Mismatched " + result + " retries count", provider.getMaxRetries(), provider.getRetriesCount());
+ assertLoadedKeyPair(encryptedFile, keys.iterator().next());
+ }
+ } catch (IOException | GeneralSecurityException | RuntimeException e) {
+ if (result != ResourceDecodeResult.TERMINATE) {
+ throw e;
+ }
+
+ assertEquals("Mismatched " + result + " retries count", 0, provider.getRetriesCount());
+ }
+ }
+ }
+
private void assertLoadedKeyPair(String prefix, KeyPair kp) throws GeneralSecurityException {
assertNotNull(prefix + ": no key pair loaded", kp);

Loading...