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:21 UTC

[mina-sshd] branch master updated (804849f -> 89d014a)

This is an automated email from the ASF dual-hosted git repository.

lgoldstein pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git.


    from 804849f  Remove wrong override annotation again!
     new a1d767f  [SSHD-1247] Added support for Argon2id encrypted PUTTY keys
     new 89d014a  Organized POM properties in a more consistent manner

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 CHANGES.md                                         |   1 +
 assembly/src/main/legal/notices.xml                |   2 +-
 pom.xml                                            |   4 +-
 .../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    |  44 ++++---
 ...ialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk |  31 +++++
 13 files changed, 306 insertions(+), 94 deletions(-)
 create mode 100644 sshd-putty/src/test/java/org/apache/sshd/putty/AbstractPuttyTestSupport.java
 copy sshd-common/src/test/java/org/apache/sshd/common/util/io/der/DERParserTest.java => sshd-putty/src/test/java/org/apache/sshd/putty/PuttySpecialKeysTest.java (52%)
 create mode 100644 sshd-putty/src/test/resources/org/apache/sshd/putty/PuttySpecialKeysTest-ssh-rsa-argon2id-KeyPair-123456.ppk

[mina-sshd] 02/02: Organized POM properties in a more consistent manner

Posted by lg...@apache.org.
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 89d014af66d471ae539615f6ad55a58e958632ee
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Feb 18 12:11:54 2022 +0200

    Organized POM properties in a more consistent manner
---
 pom.xml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/pom.xml b/pom.xml
index 7e66769..1fd13e9 100644
--- a/pom.xml
+++ b/pom.xml
@@ -99,8 +99,8 @@
         <ant.build.javac.source>${javac.source}</ant.build.javac.source>
         <build-helper-maven-plugin.version>3.2.0</build-helper-maven-plugin.version>
 
-        <required.java.version>[${javac.target},)</required.java.version>
         <javac.target>${javac.source}</javac.target>
+        <required.java.version>[${javac.target},)</required.java.version>
         <project.build.java.target>${javac.target}</project.build.java.target>
         <maven.compiler.target>${javac.target}</maven.compiler.target>
         <ant.build.javac.target>${javac.target}</ant.build.javac.target>
@@ -717,7 +717,7 @@
                             <exclude>*findbugs*</exclude>
                             <exclude>*suppressions*</exclude>
                             <exclude>**/big-msg.txt</exclude>
-                            <!-- Special file imported from another project that we want to keep as-is -->
+                            <!-- Special files imported from another project that we want to keep as-is -->
                             <exclude>**/BCrypt.java</exclude>
                             <exclude>LICENSE*</exclude>
                             <exclude>NOTICE*</exclude>

[mina-sshd] 01/02: [SSHD-1247] Added support for Argon2id encrypted PUTTY keys

Posted by lg...@apache.org.
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