You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2022/02/19 15:11:22 UTC
[mina-sshd] 01/02: [SSHD-1247] Added support for Argon2id encrypted PUTTY keys
This is an automated email from the ASF dual-hosted git repository.
lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
commit a1d767f3595f3974c37cc747825207cd47756a67
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Feb 18 12:11:31 2022 +0200
[SSHD-1247] Added support for Argon2id encrypted PUTTY keys
---
CHANGES.md | 1 +
assembly/src/main/legal/notices.xml | 2 +-
.../apache/sshd/putty/AbstractPuttyKeyDecoder.java | 38 ++++--
.../org/apache/sshd/putty/DSSPuttyKeyDecoder.java | 3 +-
.../apache/sshd/putty/ECDSAPuttyKeyDecoder.java | 3 +-
.../apache/sshd/putty/EdDSAPuttyKeyDecoder.java | 3 +-
.../sshd/putty/PuttyKeyPairResourceParser.java | 127 ++++++++++++++++++---
.../org/apache/sshd/putty/RSAPuttyKeyDecoder.java | 3 +-
.../sshd/putty/AbstractPuttyTestSupport.java | 100 ++++++++++++++++
.../org/apache/sshd/putty/PuttyKeyUtilsTest.java | 41 +------
.../apache/sshd/putty/PuttySpecialKeysTest.java | 59 ++++++++++
...ialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk | 31 +++++
12 files changed, 342 insertions(+), 69 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index fb283e5..857da14 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -70,5 +70,6 @@ Was originally in *HostConfigEntry*.
* [SSHD-1244](https://issues.apache.org/jira/browse/SSHD-1244) Fixed channel window adjustment handling of large UINT32 values
* [SSHD-1244](https://issues.apache.org/jira/browse/SSHD-1244) Re-defined channel identifiers as `long` rather than `int` to align with protocol UINT32 definition
* [SSHD-1246](https://issues.apache.org/jira/browse/SSHD-1246) Added SshKeyDumpMain utility
+* [SSHD-1247](https://issues.apache.org/jira/browse/SSHD-1247) Added support for Argon2id encrypted PUTTY keys
diff --git a/assembly/src/main/legal/notices.xml b/assembly/src/main/legal/notices.xml
index 6a60df2..f6c3fba 100644
--- a/assembly/src/main/legal/notices.xml
+++ b/assembly/src/main/legal/notices.xml
@@ -149,7 +149,7 @@
<licenses>
<license>
<name>The Apache Software License, Version 2.0</name>
- <url>https://github.com/spring-projects/spring-integration/blob/master/src/dist/license.txt</url>
+ <url>https://github.com/spring-projects/spring-integration/blob/main/LICENSE.txt</url>
</license>
</licenses>
</project>
diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/AbstractPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/AbstractPuttyKeyDecoder.java
index 7331c0e..263a164 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/putty/AbstractPuttyKeyDecoder.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/putty/AbstractPuttyKeyDecoder.java
@@ -28,6 +28,7 @@ import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Base64;
import java.util.Base64.Decoder;
@@ -96,6 +97,7 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
List<String> prvLines = Collections.emptyList();
Map<String, String> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
String prvEncryption = null;
+ int formatVersion = -1;
for (int index = 0, numLines = lines.size(); index < numLines; index++) {
String l = lines.get(index);
l = GenericUtils.trimToEmpty(l);
@@ -107,6 +109,16 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
String hdrName = l.substring(0, pos).trim();
String hdrValue = l.substring(pos + 1).trim();
headers.put(hdrName, hdrValue);
+ if (hdrName.startsWith(KEY_FILE_HEADER_PREFIX)) {
+ String versionValue = hdrName.substring(KEY_FILE_HEADER_PREFIX.length());
+ int fileVersion = Integer.parseInt(versionValue);
+ if ((formatVersion >= 0) && (fileVersion != formatVersion)) {
+ throw new InvalidKeySpecException(
+ "Inconsistent key file version specification: " + formatVersion + " and " + fileVersion);
+ }
+ formatVersion = fileVersion;
+ }
+
switch (hdrName) {
case ENCRYPTION_HEADER:
if (prvEncryption != null) {
@@ -126,7 +138,7 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
}
}
- return loadKeyPairs(session, resourceKey, pubLines, prvLines, prvEncryption, passwordProvider, headers);
+ return loadKeyPairs(session, resourceKey, formatVersion, pubLines, prvLines, prvEncryption, passwordProvider, headers);
}
public static List<String> extractDataLines(
@@ -154,17 +166,17 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
}
public Collection<KeyPair> loadKeyPairs(
- SessionContext session, NamedResource resourceKey,
+ SessionContext session, NamedResource resourceKey, int formatVersion,
List<String> pubLines, List<String> prvLines, String prvEncryption,
FilePasswordProvider passwordProvider, Map<String, String> headers)
throws IOException, GeneralSecurityException {
- return loadKeyPairs(session, resourceKey,
+ return loadKeyPairs(session, resourceKey, formatVersion,
KeyPairResourceParser.joinDataLines(pubLines), KeyPairResourceParser.joinDataLines(prvLines),
prvEncryption, passwordProvider, headers);
}
public Collection<KeyPair> loadKeyPairs(
- SessionContext session, NamedResource resourceKey,
+ SessionContext session, NamedResource resourceKey, int formatVersion,
String pubData, String prvData, String prvEncryption,
FilePasswordProvider passwordProvider, Map<String, String> headers)
throws IOException, GeneralSecurityException {
@@ -176,7 +188,7 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
prvBytes = b64Decoder.decode(prvData);
if (GenericUtils.isEmpty(prvEncryption)
|| NO_PRIVATE_KEY_ENCRYPTION_VALUE.equalsIgnoreCase(prvEncryption)) {
- return loadKeyPairs(resourceKey, pubBytes, prvBytes, headers);
+ return loadKeyPairs(resourceKey, formatVersion, pubBytes, prvBytes, headers);
}
// format is "<cipher><bits>-<mode>" - e.g., "aes256-cbc"
@@ -211,9 +223,9 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
}
byte[] decBytes = PuttyKeyPairResourceParser.decodePrivateKeyBytes(
- prvBytes, algName, numBits, mode, password);
+ formatVersion, prvBytes, algName, numBits, mode, password, headers);
try {
- keys = loadKeyPairs(resourceKey, pubBytes, decBytes, headers);
+ keys = loadKeyPairs(resourceKey, formatVersion, pubBytes, decBytes, headers);
} finally {
Arrays.fill(decBytes, (byte) 0); // eliminate sensitive data a.s.a.p.
}
@@ -250,28 +262,30 @@ public abstract class AbstractPuttyKeyDecoder<PUB extends PublicKey, PRV extends
}
public Collection<KeyPair> loadKeyPairs(
- NamedResource resourceKey, byte[] pubData, byte[] prvData, Map<String, String> headers)
+ NamedResource resourceKey, int formatVersion, byte[] pubData, byte[] prvData, Map<String, String> headers)
throws IOException, GeneralSecurityException {
ValidateUtils.checkNotNullAndNotEmpty(pubData, "No public key data in %s", resourceKey);
ValidateUtils.checkNotNullAndNotEmpty(prvData, "No private key data in %s", resourceKey);
try (InputStream pubStream = new ByteArrayInputStream(pubData);
InputStream prvStream = new ByteArrayInputStream(prvData)) {
- return loadKeyPairs(resourceKey, pubStream, prvStream, headers);
+ return loadKeyPairs(resourceKey, formatVersion, pubStream, prvStream, headers);
}
}
public Collection<KeyPair> loadKeyPairs(
- NamedResource resourceKey, InputStream pubData, InputStream prvData, Map<String, String> headers)
+ NamedResource resourceKey, int formatVersion,
+ InputStream pubData, InputStream prvData, Map<String, String> headers)
throws IOException, GeneralSecurityException {
try (PuttyKeyReader pubReader
= new PuttyKeyReader(ValidateUtils.checkNotNull(pubData, "No public key data in %s", resourceKey));
PuttyKeyReader prvReader
= new PuttyKeyReader(ValidateUtils.checkNotNull(prvData, "No private key data in %s", resourceKey))) {
- return loadKeyPairs(resourceKey, pubReader, prvReader, headers);
+ return loadKeyPairs(resourceKey, formatVersion, pubReader, prvReader, headers);
}
}
public abstract Collection<KeyPair> loadKeyPairs(
- NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers)
+ NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader,
+ Map<String, String> headers)
throws IOException, GeneralSecurityException;
}
diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/DSSPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/DSSPuttyKeyDecoder.java
index ebc342a..04426cd 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/putty/DSSPuttyKeyDecoder.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/putty/DSSPuttyKeyDecoder.java
@@ -51,7 +51,8 @@ public class DSSPuttyKeyDecoder extends AbstractPuttyKeyDecoder<DSAPublicKey, DS
@Override
public Collection<KeyPair> loadKeyPairs(
- NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers)
+ NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader,
+ Map<String, String> headers)
throws IOException, GeneralSecurityException {
pubReader.skip(); // skip version
diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/ECDSAPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/ECDSAPuttyKeyDecoder.java
index ce7a298..48bc921 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/putty/ECDSAPuttyKeyDecoder.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/putty/ECDSAPuttyKeyDecoder.java
@@ -57,7 +57,8 @@ public class ECDSAPuttyKeyDecoder extends AbstractPuttyKeyDecoder<ECPublicKey, E
@Override
public Collection<KeyPair> loadKeyPairs(
- NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers)
+ NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader,
+ Map<String, String> headers)
throws IOException, GeneralSecurityException {
if (!SecurityUtils.isECCSupported()) {
throw new NoSuchAlgorithmException("ECC not supported for " + resourceKey);
diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/EdDSAPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/EdDSAPuttyKeyDecoder.java
index 1bef3a3..c427002 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/putty/EdDSAPuttyKeyDecoder.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/putty/EdDSAPuttyKeyDecoder.java
@@ -50,7 +50,8 @@ public class EdDSAPuttyKeyDecoder extends AbstractPuttyKeyDecoder<EdDSAPublicKey
@Override
public Collection<KeyPair> loadKeyPairs(
- NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers)
+ NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader,
+ Map<String, String> headers)
throws IOException, GeneralSecurityException {
if (!SecurityUtils.isEDDSACurveSupported()) {
throw new NoSuchAlgorithmException(SecurityUtils.EDDSA + " provider not supported for " + resourceKey);
diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/PuttyKeyPairResourceParser.java b/sshd-putty/src/main/java/org/apache/sshd/putty/PuttyKeyPairResourceParser.java
index ae57afa..912695c 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/putty/PuttyKeyPairResourceParser.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/putty/PuttyKeyPairResourceParser.java
@@ -31,6 +31,7 @@ import java.security.spec.InvalidKeySpecException;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import javax.crypto.Cipher;
@@ -42,8 +43,12 @@ import org.apache.sshd.common.config.keys.IdentityResourceLoader;
import org.apache.sshd.common.config.keys.loader.KeyPairResourceParser;
import org.apache.sshd.common.digest.BuiltinDigests;
import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.MapEntryUtils;
import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
import org.apache.sshd.common.util.security.SecurityUtils;
+import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
+import org.bouncycastle.crypto.params.Argon2Parameters;
//CHECKSTYLE:OFF
/**
@@ -95,7 +100,7 @@ import org.apache.sshd.common.util.security.SecurityUtils;
//CHECKSTYLE:ON
public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends PrivateKey>
extends IdentityResourceLoader<PUB, PRV>, KeyPairResourceParser {
- String KEY_FILE_HEADER_PREFIX = "PuTTY-User-Key-File";
+ String KEY_FILE_HEADER_PREFIX = "PuTTY-User-Key-File-";
String PUBLIC_LINES_HEADER = "Public-Lines";
String PRIVATE_LINES_HEADER = "Private-Lines";
String PPK_FILE_SUFFIX = ".ppk";
@@ -111,6 +116,9 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P
*/
String NO_PRIVATE_KEY_ENCRYPTION_VALUE = "none";
+ /** PUTTY key v3 MAC key length */
+ int FORMAT_3_MAC_KEY_LENGTH = 32;
+
@Override
default boolean canExtractKeyPairs(NamedResource resourceKey, List<String> lines)
throws IOException, GeneralSecurityException {
@@ -131,7 +139,8 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P
}
static byte[] decodePrivateKeyBytes(
- byte[] prvBytes, String algName, int numBits, String algMode, String password)
+ int formatVersion, byte[] prvBytes, String algName, int numBits, String algMode, String password,
+ Map<String, String> headers)
throws GeneralSecurityException {
Objects.requireNonNull(prvBytes, "No encrypted key bytes");
ValidateUtils.checkNotNullAndNotEmpty(algName, "No encryption algorithm", GenericUtils.EMPTY_OBJECT_ARRAY);
@@ -143,8 +152,13 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P
throw new NoSuchAlgorithmException("decodePrivateKeyBytes(" + algName + "-" + numBits + "-" + algMode + ") N/A");
}
+ if ((numBits != 128) && (numBits != 192) && (numBits != 256)) {
+ throw new InvalidKeySpecException("Requested key size (" + numBits + ") is not supported");
+ }
+
byte[] initVector = new byte[16];
- byte[] keyValue = toEncryptionKey(password);
+ byte[] keyValue = new byte[numBits / Byte.SIZE];
+ decodeEncryptionKey(formatVersion, password, initVector, keyValue, headers);
try {
return decodePrivateKeyBytes(prvBytes, algName, algMode, numBits, initVector, keyValue);
} finally {
@@ -175,28 +189,111 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P
}
/**
- * Converts a pass-phrase into a key, by following the convention that PuTTY uses. Used to decrypt the private key
+ * Converts a pass-phrase into a key, by following the conventions that PuTTY uses. Used to decrypt the private key
* when it's encrypted.
- *
- * @param passphrase the Password to be used as seed for the key - ignored if {@code null}/empty
- * @return The encryption key bytes - {@code null/empty} if no pass-phrase
+ *
+ * @param formatVersion The file format version
+ * @param passphrase The Password to be used as seed for the key - ignored if {@code null}/empty
+ * @param iv Initialization vector to be populated if necessary
+ * @param key Key to be populated
+ * @param headers Any extra headers found in the PPK file that might be used for KDF
+ * @throws GeneralSecurityException If cannot derive the key bytes from the password
+ */
+ static void decodeEncryptionKey(
+ int formatVersion, String passphrase, byte[] iv, byte[] key, Map<String, String> headers)
+ throws GeneralSecurityException {
+ String keyDerivationType = getStringHeaderValue(headers, "Key-Derivation");
+ if (GenericUtils.isBlank(keyDerivationType)) {
+ deriveFormat2EncryptionKey(passphrase, iv, key);
+ } else if ("Argon2id".equalsIgnoreCase(keyDerivationType)
+ || "Argon2i".equalsIgnoreCase(keyDerivationType)
+ || "Argon2d".equalsIgnoreCase(keyDerivationType)) {
+ deriveFormat3EncryptionKey(passphrase, keyDerivationType, iv, key, headers);
+ } else {
+ throw new NoSuchAlgorithmException("Unsupported KDF method: " + keyDerivationType);
+ }
+ }
+
+ static void deriveFormat3EncryptionKey(
+ String passphrase, String keyDerivationType, byte[] iv, byte[] key, Map<String, String> headers)
+ throws GeneralSecurityException {
+ ValidateUtils.checkNotNullAndNotEmpty(headers, "Mising file headers for KDF purposes");
+ Objects.requireNonNull(passphrase, "No passphrase provded");
+
+ int parallelism = getIntegerHeaderValue(headers, "Argon2-Parallelism");
+ int iterations = getIntegerHeaderValue(headers, "Argon2-Passes");
+ int memory = getIntegerHeaderValue(headers, "Argon2-Memory");
+ byte[] salt = ValidateUtils.checkNotNullAndNotEmpty(
+ getHexArrayHeaderValue(headers, "Argon2-Salt"), "No Argon2 salt value provided");
+ byte[] hashValue = new byte[key.length + iv.length + FORMAT_3_MAC_KEY_LENGTH];
+ byte[] passBytes = passphrase.getBytes(StandardCharsets.UTF_8);
+ try {
+ Argon2Parameters.Builder builder;
+ if ("Argon2id".equalsIgnoreCase(keyDerivationType)) {
+ builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id);
+ } else if ("Argon2i".equalsIgnoreCase(keyDerivationType)) {
+ builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i);
+ } else if ("Argon2d".equalsIgnoreCase(keyDerivationType)) {
+ builder = new Argon2Parameters.Builder(Argon2Parameters.ARGON2_i);
+ } else {
+ throw new NoSuchAlgorithmException("Unsupported key derivation type: " + keyDerivationType);
+ }
+ Argon2Parameters params = builder
+ .withSalt(salt)
+ .withParallelism(parallelism)
+ .withMemoryAsKB(memory)
+ .withIterations(iterations)
+ .build();
+ Argon2BytesGenerator generator = new Argon2BytesGenerator();
+ generator.init(params);
+ generator.generateBytes(passBytes, hashValue);
+ } finally {
+ Arrays.fill(passBytes, (byte) 0); // eliminate sensitive data a.s.a.p.
+ }
+
+ try {
+ System.arraycopy(hashValue, 0, key, 0, key.length);
+ System.arraycopy(hashValue, key.length, iv, 0, iv.length);
+ } finally {
+ Arrays.fill(hashValue, (byte) 0); // eliminate sensitive data a.s.a.p.
+ }
+ }
+
+ static String getStringHeaderValue(Map<String, String> headers, String key) {
+ return MapEntryUtils.isEmpty(headers) ? null : headers.get(key);
+ }
+
+ static byte[] getHexArrayHeaderValue(Map<String, String> headers, String key) {
+ String value = getStringHeaderValue(headers, key);
+ return BufferUtils.decodeHex(BufferUtils.EMPTY_HEX_SEPARATOR, value);
+ }
+
+ static int getIntegerHeaderValue(Map<String, String> headers, String key) {
+ String value
+ = ValidateUtils.checkNotNullAndNotEmpty(getStringHeaderValue(headers, key), "Missing %s header value", key);
+ return Integer.parseInt(value);
+ }
+
+ /**
+ * Uses the "legacy" KDF via SHA-1
+ *
+ * @param passphrase The Password to be used as seed for the key - ignored if {@code null}/empty
+ * @param iv Initialization vector to be populated if necessary
+ * @param key Key to be populated
* @throws GeneralSecurityException If cannot retrieve SHA-1 digest
* @see <A HREF=
* "http://security.stackexchange.com/questions/71341/how-does-putty-derive-the-encryption-key-in-its-ppk-format">
* How does Putty derive the encryption key in its .ppk format ?</A>
*/
- static byte[] toEncryptionKey(String passphrase) throws GeneralSecurityException {
- if (GenericUtils.isEmpty(passphrase)) {
- return GenericUtils.EMPTY_BYTE_ARRAY;
- }
+ static void deriveFormat2EncryptionKey(String passphrase, byte[] iv, byte[] key) throws GeneralSecurityException {
+ Objects.requireNonNull(passphrase, "No passphrase provded");
byte[] passBytes = passphrase.getBytes(StandardCharsets.UTF_8);
try {
MessageDigest hash = SecurityUtils.getMessageDigest(BuiltinDigests.sha1.getAlgorithm());
byte[] stateValue = { 0, 0, 0, 0 };
- byte[] keyValue = new byte[32];
try {
- for (int i = 0, remLen = keyValue.length; i < 2; i++) {
+ for (int i = 0, remLen = key.length; remLen > 0; i++) {
hash.reset(); // just making sure
stateValue[3] = (byte) i;
@@ -205,7 +302,7 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P
byte[] digest = hash.digest();
try {
- System.arraycopy(digest, 0, keyValue, i * 20, Math.min(20, remLen));
+ System.arraycopy(digest, 0, key, i * 20, Math.min(20, remLen));
} finally {
Arrays.fill(digest, (byte) 0); // eliminate sensitive data a.s.a.p.
}
@@ -215,7 +312,7 @@ public interface PuttyKeyPairResourceParser<PUB extends PublicKey, PRV extends P
Arrays.fill(stateValue, (byte) 0); // eliminate sensitive data a.s.a.p.
}
- return keyValue;
+ Arrays.fill(iv, (byte) 0);
} finally {
Arrays.fill(passBytes, (byte) 0); // eliminate sensitive data a.s.a.p.
}
diff --git a/sshd-putty/src/main/java/org/apache/sshd/putty/RSAPuttyKeyDecoder.java b/sshd-putty/src/main/java/org/apache/sshd/putty/RSAPuttyKeyDecoder.java
index 998b9bd..6950ef2 100644
--- a/sshd-putty/src/main/java/org/apache/sshd/putty/RSAPuttyKeyDecoder.java
+++ b/sshd-putty/src/main/java/org/apache/sshd/putty/RSAPuttyKeyDecoder.java
@@ -52,7 +52,8 @@ public class RSAPuttyKeyDecoder extends AbstractPuttyKeyDecoder<RSAPublicKey, RS
@Override
public Collection<KeyPair> loadKeyPairs(
- NamedResource resourceKey, PuttyKeyReader pubReader, PuttyKeyReader prvReader, Map<String, String> headers)
+ NamedResource resourceKey, int formatVersion, PuttyKeyReader pubReader, PuttyKeyReader prvReader,
+ Map<String, String> headers)
throws IOException, GeneralSecurityException {
pubReader.skip(); // skip version
diff --git a/sshd-putty/src/test/java/org/apache/sshd/putty/AbstractPuttyTestSupport.java b/sshd-putty/src/test/java/org/apache/sshd/putty/AbstractPuttyTestSupport.java
new file mode 100644
index 0000000..ed61230
--- /dev/null
+++ b/sshd-putty/src/test/java/org/apache/sshd/putty/AbstractPuttyTestSupport.java
@@ -0,0 +1,100 @@
+/*
+ * 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.putty;
+
+import java.io.IOException;
+import java.net.URL;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.Collection;
+
+import org.apache.sshd.common.cipher.BuiltinCiphers;
+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.util.test.JUnitTestSupport;
+import org.junit.Assume;
+import org.junit.AssumptionViolatedException;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractPuttyTestSupport extends JUnitTestSupport {
+ protected AbstractPuttyTestSupport() {
+ super();
+ }
+
+ protected KeyPair testDecodeEncryptedPuttyKeyFile(
+ String encryptedFile, boolean okIfMissing, String password, String keyType)
+ throws IOException, GeneralSecurityException {
+ PuttyKeyPairResourceParser<?, ?> parser = PuttyKeyUtils.BY_KEY_TYPE.get(keyType);
+ assertNotNull("No parser found for key type=" + keyType, parser);
+ return testDecodeEncryptedPuttyKeyFile(encryptedFile, okIfMissing, password, parser, keyType);
+ }
+
+ protected KeyPair testDecodeEncryptedPuttyKeyFile(
+ String encryptedFile, boolean okIfMissing, String password, PuttyKeyPairResourceParser<?, ?> parser, String keyType)
+ throws IOException, GeneralSecurityException {
+ Assume.assumeTrue(BuiltinCiphers.aes256cbc.getTransformation() + " N/A", BuiltinCiphers.aes256cbc.isSupported());
+
+ URL url = getClass().getResource(encryptedFile);
+ if (url == null) {
+ if (okIfMissing) {
+ throw new AssumptionViolatedException("Skip non-existent encrypted file: " + encryptedFile);
+ }
+
+ fail("Missing test resource: " + encryptedFile);
+ }
+
+ Collection<KeyPair> keys = parser.loadKeyPairs(null, url, (s, r, index) -> password);
+ assertEquals("Mismatched loaded keys count from " + encryptedFile, 1, GenericUtils.size(keys));
+
+ return assertLoadedKeyPair(encryptedFile, GenericUtils.head(keys), keyType);
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////////
+
+ public static KeyPair assertLoadedKeyPair(String prefix, KeyPair kp, String keyType) throws GeneralSecurityException {
+ assertNotNull(prefix + ": no key pair loaded", kp);
+
+ PublicKey pubKey = kp.getPublic();
+ assertNotNull(prefix + ": no public key loaded", pubKey);
+ assertEquals(prefix + ": mismatched public key type", keyType, KeyUtils.getKeyType(pubKey));
+
+ PrivateKey prvKey = kp.getPrivate();
+ assertNotNull(prefix + ": no private key loaded", prvKey);
+ assertEquals(prefix + ": mismatched private key type", keyType, KeyUtils.getKeyType(prvKey));
+
+ @SuppressWarnings("rawtypes")
+ PrivateKeyEntryDecoder decoder = OpenSSHKeyPairResourceParser.getPrivateKeyEntryDecoder(prvKey);
+ assertNotNull("No private key decoder", decoder);
+
+ if (decoder.isPublicKeyRecoverySupported()) {
+ @SuppressWarnings("unchecked")
+ PublicKey recKey = decoder.recoverPublicKey(prvKey);
+ assertKeyEquals("Mismatched recovered public key", pubKey, recKey);
+ }
+
+ return kp;
+ }
+}
diff --git a/sshd-putty/src/test/java/org/apache/sshd/putty/PuttyKeyUtilsTest.java b/sshd-putty/src/test/java/org/apache/sshd/putty/PuttyKeyUtilsTest.java
index 25dd011..24803c9 100644
--- a/sshd-putty/src/test/java/org/apache/sshd/putty/PuttyKeyUtilsTest.java
+++ b/sshd-putty/src/test/java/org/apache/sshd/putty/PuttyKeyUtilsTest.java
@@ -23,8 +23,6 @@ import java.io.IOException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
-import java.security.PrivateKey;
-import java.security.PublicKey;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
@@ -33,15 +31,11 @@ import org.apache.sshd.common.NamedResource;
import org.apache.sshd.common.cipher.BuiltinCiphers;
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.PrivateKeyEntryDecoder;
-import org.apache.sshd.common.config.keys.loader.openssh.OpenSSHKeyPairResourceParser;
import org.apache.sshd.common.session.SessionContext;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.functors.UnaryEquator;
import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
-import org.apache.sshd.util.test.JUnitTestSupport;
import org.apache.sshd.util.test.NoIoTestCase;
import org.junit.Assume;
import org.junit.FixMethodOrder;
@@ -61,7 +55,7 @@ import org.mockito.Mockito;
@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests
@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
@Category({ NoIoTestCase.class })
-public class PuttyKeyUtilsTest extends JUnitTestSupport {
+public class PuttyKeyUtilsTest extends AbstractPuttyTestSupport {
public static final String PASSWORD = "super secret passphrase";
private final String keyType;
@@ -125,16 +119,7 @@ public class PuttyKeyUtilsTest extends JUnitTestSupport {
@Test
public void testDecodeEncryptedPuttyKeyFile() 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);
-
- Collection<KeyPair> keys = parser.loadKeyPairs(null, url, (s, r, index) -> PASSWORD);
- assertEquals("Mismatched loaded keys count from " + encryptedFile, 1, GenericUtils.size(keys));
-
- assertLoadedKeyPair(encryptedFile, GenericUtils.head(keys));
+ testDecodeEncryptedPuttyKeyFile(encryptedFile, true, PASSWORD, parser, keyType);
}
@Test
@@ -219,25 +204,7 @@ public class PuttyKeyUtilsTest extends JUnitTestSupport {
}
}
- private void assertLoadedKeyPair(String prefix, KeyPair kp) throws GeneralSecurityException {
- assertNotNull(prefix + ": no key pair loaded", kp);
-
- PublicKey pubKey = kp.getPublic();
- assertNotNull(prefix + ": no public key loaded", pubKey);
- assertEquals(prefix + ": mismatched public key type", keyType, KeyUtils.getKeyType(pubKey));
-
- PrivateKey prvKey = kp.getPrivate();
- assertNotNull(prefix + ": no private key loaded", prvKey);
- assertEquals(prefix + ": mismatched private key type", keyType, KeyUtils.getKeyType(prvKey));
-
- @SuppressWarnings("rawtypes")
- PrivateKeyEntryDecoder decoder = OpenSSHKeyPairResourceParser.getPrivateKeyEntryDecoder(prvKey);
- assertNotNull("No private key decoder", decoder);
-
- if (decoder.isPublicKeyRecoverySupported()) {
- @SuppressWarnings("unchecked")
- PublicKey recKey = decoder.recoverPublicKey(prvKey);
- assertKeyEquals("Mismatched recovered public key", pubKey, recKey);
- }
+ private KeyPair assertLoadedKeyPair(String prefix, KeyPair kp) throws GeneralSecurityException {
+ return assertLoadedKeyPair(prefix, kp, keyType);
}
}
diff --git a/sshd-putty/src/test/java/org/apache/sshd/putty/PuttySpecialKeysTest.java b/sshd-putty/src/test/java/org/apache/sshd/putty/PuttySpecialKeysTest.java
new file mode 100644
index 0000000..35f8455
--- /dev/null
+++ b/sshd-putty/src/test/java/org/apache/sshd/putty/PuttySpecialKeysTest.java
@@ -0,0 +1,59 @@
+/*
+ * 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.putty;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+
+import org.apache.sshd.common.util.security.SecurityUtils;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.Assume;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runners.MethodSorters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Category({ NoIoTestCase.class })
+public class PuttySpecialKeysTest extends AbstractPuttyTestSupport {
+ public PuttySpecialKeysTest() {
+ super();
+ }
+
+ @Test // SSHD-1247
+ public void testArgon2KeyDerivation() throws Exception {
+ Assume.assumeTrue("BC provider available", SecurityUtils.isBouncyCastleRegistered());
+ testDecodeSpecialEncryptedPuttyKeyFile("ssh-rsa", "argon2id", "123456");
+ }
+
+ protected KeyPair testDecodeSpecialEncryptedPuttyKeyFile(
+ String keyType, String flavor, String password)
+ throws IOException, GeneralSecurityException {
+ return testDecodeEncryptedPuttyKeyFile(
+ getClass().getSimpleName() + "-" + keyType
+ + "-" + flavor + "-" + KeyPair.class.getSimpleName()
+ + "-" + password + PuttyKeyPairResourceParser.PPK_FILE_SUFFIX,
+ false, password, keyType);
+ }
+}
diff --git a/sshd-putty/src/test/resources/org/apache/sshd/putty/PuttySpecialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk b/sshd-putty/src/test/resources/org/apache/sshd/putty/PuttySpecialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk
new file mode 100644
index 0000000..7758fdc
--- /dev/null
+++ b/sshd-putty/src/test/resources/org/apache/sshd/putty/PuttySpecialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk
@@ -0,0 +1,31 @@
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: aes256-cbc
+Comment: rsa-key-20220217
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQDJqngLGC4VEPxQECFimieG28bimImxJG4N
+w6lZ8FHjQELXrd/yByHrVLBUecOSMtx8meozoWwyOaKwf3MgEm0bU8VHLG45kTfP
+X5bbAu/V+5JAC7IDsxPB8ULPk2A7K0+whSF3v08Qrsaamw9ZH/IP1prBDyLRBaYD
+539e6NkvHhmIPmpfzf7ahmRMC2+9i7f0RxG2tGtFRA5tIMLROfczU+TQImNLWZLF
+dHII80TyhRXXruX1DuZGBR8s2hiazH4gY+cL/YUBvmZMLF2VYlVq/0peWqgcdEGQ
+MPwlPmbpqYnN1DevLQiVU8k/ztrp3mBFigdX+P6IgYgfGkeZvvp1
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 5
+Argon2-Parallelism: 1
+Argon2-Salt: 6fdbdf2093b7697f46686e31506c1564
+Private-Lines: 14
+acARqLeh0XtN0WAdOlN4G3gSDI7y8w1WvxI2ykZnh1y9J86iYo+uDpsIgdGkxNK7
+swCoogl1kqYIOFe+yOsmAeynIBwyPd42KoE4er1EqKMfyiVa5Gy9wEzTawYCnK7E
+aLmjUiJHU6kkRraD0QUuo1enfbu/xGqicRSVcVLYPR9jgQ5FN3+1lDHCo7bpj5up
+j/ErTcFmIEmpWLBbi8gF/qU3WLi3YgiFTT6tBY3f9PlehJzcMv0DqsRqE9/F9+Oc
+ZYq+/iTb95UkwfYvmKkWLqJY5Wf+it+WkGOjj5d1ti2LrBZlSd7VVaToGU5VXUzR
+TG3ZzRr7yAM6PkkINzyYcGnZNz6NnYfqq4y9QD1cAzJa+00+ngavgwybKqWwV8Rm
+RGn0RSBXsIDAsSwuj0rEax7VYKAfKuGJHdmK9FdJ/oEEPxQ+DmRCG368GhhWlLAY
+joUl6H45qhHUNF6+a0kPK9geZZR1pbhEqeKeSTHZUbwAxkTIZPX61F9GCYNkvN5o
+tVdIXXF2SgiNUdtYuKmGdNx6rOmCs2dWeH5kboQsh52gtzFpSgIEiFq/n32cG5od
+mYufM7WsWlkasIXlQV8lpd2elp5ob9gOJP5P3CGzyvtLb2myEy8HPcRBHXr7qtyT
+MEPTcjD5axegXLCCIk7FDTVKtl2oxTH9htGQYEaGG+3Yh7qh8lHT/aLrZkGW2tLJ
+AvMHrDlWyUaBdo13yJL6ywx35wBcMDPrjU/GeHT05sShEjy7CRv3vmyEYHqJNYCC
+hjFo1N2wVe18Z4pnbjDKDviZiCy1WfDkl/KidDMCD5HGo3mRZC4l3/LajrtWygmc
+0rgdX1UueHVlKcakgcJX/dLoQZvFHAc6h8GhmEYMJFmJAzITholR0aMkYaToFJPe
+Private-MAC: 7dcaa183900b3b60c1b90c689b2f4f283334c30d97abea1155320a4247ff42c8