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 &quot;legacy&quot; 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