Sfoglia il codice sorgente

[Private Key] Add support for PuTTY private key file format (V3 and V2) (#1543)

* [Private Key] Add support for PuTTY private key

* add negative test for mac

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Scott Xu 10 mesi fa
parent
commit
3b4f2cfc1c

+ 1 - 1
Directory.Packages.props

@@ -5,7 +5,7 @@
   </PropertyGroup>
   <ItemGroup>
     <PackageVersion Include="BenchmarkDotNet" Version="0.14.0" />
-    <PackageVersion Include="BouncyCastle.Cryptography" Version="2.4.0" />
+    <PackageVersion Include="BouncyCastle.Cryptography" Version="2.5.0" />
     <PackageVersion Include="coverlet.collector" Version="6.0.2" />
     <PackageVersion Include="coverlet.msbuild" Version="6.0.2" />
     <PackageVersion Include="GitHubActionsTestLogger" Version="2.4.1">

+ 8 - 1
README.md

@@ -102,17 +102,21 @@ The main types provided by this library are:
   * OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
   * ssh.com format ("BEGIN SSH2 ENCRYPTED PRIVATE KEY")
   * OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
+  * PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3")
 * DSA in
   * OpenSSL traditional PEM format ("BEGIN DSA PRIVATE KEY")
   * OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
   * ssh.com format ("BEGIN SSH2 ENCRYPTED PRIVATE KEY")
+  * PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3")
 * ECDSA 256/384/521 in
   * OpenSSL traditional PEM format ("BEGIN EC PRIVATE KEY")
   * OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
   * OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
+  * PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3")
 * ED25519 in
   * OpenSSL PKCS#8 PEM format ("BEGIN PRIVATE KEY", "BEGIN ENCRYPTED PRIVATE KEY")
   * OpenSSH key format ("BEGIN OPENSSH PRIVATE KEY")
+  * PuTTY private key format ("PuTTY-User-Key-File-2", "PuTTY-User-Key-File-3")
 
 Private keys in OpenSSL traditional PEM format can be encrypted using one of the following cipher methods:
 * DES-EDE3-CBC
@@ -124,7 +128,7 @@ Private keys in OpenSSL traditional PEM format can be encrypted using one of the
 
 Private keys in OpenSSL PKCS#8 PEM format can be encrypted using any cipher method BouncyCastle supports.
 
-Private keys in ssh.com format can be encrypted using one of the following cipher methods:
+Private keys in ssh.com format can be encrypted using the following cipher method:
 * 3des-cbc
 
 Private keys in OpenSSH key format can be encrypted using one of the following cipher methods:
@@ -139,6 +143,9 @@ Private keys in OpenSSH key format can be encrypted using one of the following c
 * aes256-gcm<span></span>@openssh.com
 * chacha20-poly1305<span></span>@openssh.com
 
+Private keys in PuTTY private key format can be encrypted using the following cipher method:
+* aes256-cbc
+
 ## Host Key Algorithms
 
 **SSH.NET** supports the following host key algorithms:

+ 5 - 7
src/Renci.SshNet/PrivateKeyFile.PKCS1.cs

@@ -42,13 +42,11 @@ namespace Renci.SshNet
                     {
                         throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
                     }
-
-                    var binarySalt = new byte[_salt.Length / 2];
-                    for (var i = 0; i < binarySalt.Length; i++)
-                    {
-                        binarySalt[i] = Convert.ToByte(_salt.Substring(i * 2, 2), 16);
-                    }
-
+#if NET
+                    var binarySalt = Convert.FromHexString(_salt);
+#else
+                    var binarySalt = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_salt);
+#endif
                     CipherInfo cipher;
                     switch (_cipherName)
                     {

+ 273 - 0
src/Renci.SshNet/PrivateKeyFile.PuTTY.cs

@@ -0,0 +1,273 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Text;
+
+using Org.BouncyCastle.Crypto.Generators;
+using Org.BouncyCastle.Crypto.Parameters;
+
+using Renci.SshNet.Abstractions;
+using Renci.SshNet.Common;
+using Renci.SshNet.Security;
+using Renci.SshNet.Security.Cryptography.Ciphers;
+
+namespace Renci.SshNet
+{
+    public partial class PrivateKeyFile
+    {
+        private sealed class PuTTY : IPrivateKeyParser
+        {
+            private readonly string _version;
+            private readonly string _algorithmName;
+            private readonly string _encryptionType;
+            private readonly string _comment;
+            private readonly byte[] _publicKey;
+            private readonly string? _argon2Type;
+            private readonly string? _argon2Salt;
+            private readonly string? _argon2Iterations;
+            private readonly string? _argon2Memory;
+            private readonly string? _argon2Parallelism;
+            private readonly byte[] _data;
+            private readonly string _mac;
+            private readonly string? _passPhrase;
+
+            public PuTTY(string version, string algorithmName, string encryptionType, string comment, byte[] publicKey, string? argon2Type, string? argon2Salt, string? argon2Iterations, string? argon2Memory, string? argon2Parallelism, byte[] data, string mac, string? passPhrase)
+            {
+                _version = version;
+                _algorithmName = algorithmName;
+                _encryptionType = encryptionType;
+                _comment = comment;
+                _publicKey = publicKey;
+                _argon2Type = argon2Type;
+                _argon2Salt = argon2Salt;
+                _argon2Iterations = argon2Iterations;
+                _argon2Memory = argon2Memory;
+                _argon2Parallelism = argon2Parallelism;
+                _data = data;
+                _mac = mac;
+                _passPhrase = passPhrase;
+            }
+
+            /// <summary>
+            /// Parses an PuTTY PPK key file.
+            /// <see href="https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html"/>.
+            /// </summary>
+            public Key Parse()
+            {
+                byte[] privateKey;
+                HMAC hmac;
+                switch (_encryptionType)
+                {
+                    case "aes256-cbc":
+                        if (string.IsNullOrEmpty(_passPhrase))
+                        {
+                            throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
+                        }
+
+                        byte[] cipherKey;
+                        byte[] cipherIV;
+                        switch (_version)
+                        {
+                            case "3":
+                                ThrowHelper.ThrowIfNullOrEmpty(_argon2Type);
+                                ThrowHelper.ThrowIfNullOrEmpty(_argon2Iterations);
+                                ThrowHelper.ThrowIfNullOrEmpty(_argon2Memory);
+                                ThrowHelper.ThrowIfNullOrEmpty(_argon2Parallelism);
+                                ThrowHelper.ThrowIfNullOrEmpty(_argon2Salt);
+
+                                var keyData = Argon2(
+                                    _argon2Type,
+                                    Convert.ToInt32(_argon2Iterations),
+                                    Convert.ToInt32(_argon2Memory),
+                                    Convert.ToInt32(_argon2Parallelism),
+#if NET
+                                    Convert.FromHexString(_argon2Salt),
+#else
+                                    Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_argon2Salt),
+#endif
+                                    _passPhrase);
+
+                                cipherKey = keyData.Take(32);
+                                cipherIV = keyData.Take(32, 16);
+
+                                var macKey = keyData.Take(48, 32);
+                                hmac = new HMACSHA256(macKey);
+
+                                break;
+                            case "2":
+                                keyData = V2KDF(_passPhrase);
+
+                                cipherKey = keyData.Take(32);
+                                cipherIV = new byte[16];
+
+                                macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key" + _passPhrase)).Take(20);
+                                hmac = new HMACSHA1(macKey);
+
+                                break;
+                            default:
+                                throw new SshException("PuTTY key file version " + _version + " is not supported");
+                        }
+
+                        using (var cipher = new AesCipher(cipherKey, cipherIV, AesCipherMode.CBC, pkcs7Padding: false))
+                        {
+                            privateKey = cipher.Decrypt(_data);
+                        }
+
+                        break;
+                    case "none":
+                        switch (_version)
+                        {
+                            case "3":
+                                hmac = new HMACSHA256(Array.Empty<byte>());
+                                break;
+                            case "2":
+                                var macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key"));
+                                hmac = new HMACSHA1(macKey);
+                                break;
+                            default:
+                                throw new SshException("PuTTY key file version " + _version + " is not supported");
+                        }
+
+                        privateKey = _data;
+                        break;
+                    default:
+                        throw new SshException("Encryption " + _encryptionType + " is not supported for PuTTY key file");
+                }
+
+                byte[] macData;
+                using (var macStream = new SshDataStream(256))
+                {
+                    macStream.Write(_algorithmName, Encoding.UTF8);
+                    macStream.Write(_encryptionType, Encoding.UTF8);
+                    macStream.Write(_comment, Encoding.UTF8);
+                    macStream.WriteBinary(_publicKey);
+                    macStream.WriteBinary(privateKey);
+                    macData = macStream.ToArray();
+                }
+
+                byte[] macValue;
+                using (hmac)
+                {
+                    macValue = hmac.ComputeHash(macData);
+                }
+#if NET
+                var reference = Convert.FromHexString(_mac);
+#else
+                var reference = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_mac);
+#endif
+                if (!macValue.SequenceEqual(reference))
+                {
+                    throw new SshException("MAC verification failed for PuTTY key file");
+                }
+
+                var publicKeyReader = new SshDataReader(_publicKey);
+                var keyType = publicKeyReader.ReadString(Encoding.UTF8);
+                Debug.Assert(keyType == _algorithmName, $"{nameof(keyType)} is not the same as {nameof(_algorithmName)}");
+
+                var privateKeyReader = new SshDataReader(privateKey);
+
+                Key parsedKey;
+
+                switch (keyType)
+                {
+                    case "ssh-ed25519":
+                        parsedKey = new ED25519Key(privateKeyReader.ReadBignum2());
+                        break;
+                    case "ecdsa-sha2-nistp256":
+                    case "ecdsa-sha2-nistp384":
+                    case "ecdsa-sha2-nistp521":
+                        var curve = publicKeyReader.ReadString(Encoding.ASCII);
+                        var pub = publicKeyReader.ReadBignum2();
+                        var prv = privateKeyReader.ReadBignum2();
+                        parsedKey = new EcdsaKey(curve, pub, prv);
+                        break;
+                    case "ssh-dss":
+                        var p = publicKeyReader.ReadBignum();
+                        var q = publicKeyReader.ReadBignum();
+                        var g = publicKeyReader.ReadBignum();
+                        var y = publicKeyReader.ReadBignum();
+                        var x = privateKeyReader.ReadBignum();
+                        parsedKey = new DsaKey(p, q, g, y, x);
+                        break;
+                    case "ssh-rsa":
+                        var exponent = publicKeyReader.ReadBignum(); // e
+                        var modulus = publicKeyReader.ReadBignum(); // n
+                        var d = privateKeyReader.ReadBignum(); // d
+                        p = privateKeyReader.ReadBignum(); // p
+                        q = privateKeyReader.ReadBignum(); // q
+                        var inverseQ = privateKeyReader.ReadBignum(); // iqmp
+                        parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
+                        break;
+                    default:
+                        throw new SshException("Key type " + keyType + " is not supported for PuTTY key file");
+                }
+
+                parsedKey.Comment = _comment;
+                return parsedKey;
+            }
+
+            private static byte[] Argon2(string type, int iterations, int memory, int parallelism, byte[] salt, string passPhrase)
+            {
+                int param;
+                switch (type)
+                {
+                    case "Argon2i":
+                        param = Argon2Parameters.Argon2i;
+                        break;
+                    case "Argon2d":
+                        param = Argon2Parameters.Argon2d;
+                        break;
+                    case "Argon2id":
+                        param = Argon2Parameters.Argon2id;
+                        break;
+                    default:
+                        throw new SshException("KDF " + type + " is not supported for PuTTY key file");
+                }
+
+                var a2p = new Argon2Parameters.Builder(param)
+                    .WithVersion(Argon2Parameters.Version13)
+                    .WithIterations(iterations)
+                    .WithMemoryAsKB(memory)
+                    .WithParallelism(parallelism)
+                    .WithSalt(salt).Build();
+
+                var generator = new Argon2BytesGenerator();
+
+                generator.Init(a2p);
+
+                var output = new byte[80];
+                var bytes = generator.GenerateBytes(passPhrase.ToCharArray(), output);
+
+                if (bytes != output.Length)
+                {
+                    throw new SshException("Failed to generate key via Argon2");
+                }
+
+                return output;
+            }
+
+            private static byte[] V2KDF(string passPhrase)
+            {
+                var cipherKey = new List<byte>();
+
+                var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
+                for (var sequenceNumber = 0; sequenceNumber < 2; sequenceNumber++)
+                {
+                    using (var sha1 = SHA1.Create())
+                    {
+                        var sequence = new byte[] { 0, 0, 0, (byte)sequenceNumber };
+                        _ = sha1.TransformBlock(sequence, 0, 4, outputBuffer: null, 0);
+                        _ = sha1.TransformFinalBlock(passPhraseBytes, 0, passPhraseBytes.Length);
+                        Debug.Assert(sha1.Hash != null, "Hash is null");
+                        cipherKey.AddRange(sha1.Hash);
+                    }
+                }
+
+                return cipherKey.ToArray();
+            }
+        }
+    }
+}

+ 55 - 6
src/Renci.SshNet/PrivateKeyFile.cs

@@ -25,16 +25,16 @@ namespace Renci.SshNet
     /// The following private keys are supported:
     /// <list type="bullet">
     ///     <item>
-    ///         <description>RSA in OpenSSL PEM, ssh.com and OpenSSH key format</description>
+    ///         <description>RSA in OpenSSL PEM, ssh.com, OpenSSH and PuTTY key format</description>
     ///     </item>
     ///     <item>
-    ///         <description>DSA in OpenSSL PEM and ssh.com format</description>
+    ///         <description>DSA in OpenSSL PEM, ssh.com and PuTTY key format</description>
     ///     </item>
     ///     <item>
-    ///         <description>ECDSA 256/384/521 in OpenSSL PEM and OpenSSH key format</description>
+    ///         <description>ECDSA 256/384/521 in OpenSSL PEM, OpenSSH and PuTTY key format</description>
     ///     </item>
     ///     <item>
-    ///         <description>ED25519 in OpenSSL PEM and OpenSSH key format</description>
+    ///         <description>ED25519 in OpenSSL PEM, OpenSSH and PuTTY key format</description>
     ///     </item>
     /// </list>
     /// </para>
@@ -73,7 +73,7 @@ namespace Renci.SshNet
     /// </list>
     /// </para>
     /// <para>
-    /// The following encryption algorithms are supported for OpenSSH format:
+    /// The following encryption algorithms are supported for OpenSSH key format:
     /// <list type="bullet">
     ///     <item>
     ///         <description>3des-cbc</description>
@@ -107,23 +107,37 @@ namespace Renci.SshNet
     ///     </item>
     /// </list>
     /// </para>
+    /// <para>
+    /// The following encryption algorithms are supported for PuTTY key format:
+    /// <list type="bullet">
+    ///     <item>
+    ///         <description>aes256-cbc</description>
+    ///     </item>
+    /// </list>
+    /// </para>
     /// </remarks>
     public partial class PrivateKeyFile : IPrivateKeySource, IDisposable
     {
         private const string PrivateKeyPattern = @"^-+ *BEGIN (?<keyName>\w+( \w+)*) *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[a-fA-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)(\r?\n)?-+ *END \k<keyName> *-+";
+        private const string PuTTYPrivateKeyPattern = @"^(?<keyName>PuTTY-User-Key-File)-(?<version>\d+): (?<algorithmName>[\w-]+)\r?\nEncryption: (?<encryptionType>[\w-]+)\r?\nComment: (?<comment>.*)\r?\nPublic-Lines: \d+\r?\n(?<publicKey>(([a-zA-Z0-9/+=]{1,64})\r?\n)+)(Key-Derivation: (?<argon2Type>\w+)\r?\nArgon2-Memory: (?<argon2Memory>\d+)\r?\nArgon2-Passes: (?<argon2Passes>\d+)\r?\nArgon2-Parallelism: (?<argon2Parallelism>\d+)\r?\nArgon2-Salt: (?<argon2Salt>[a-fA-F0-9]+)\r?\n)?Private-Lines: \d+\r?\n(?<data>(([a-zA-Z0-9/+=]{1,64})\r?\n)+)+Private-MAC: (?<mac>[a-fA-F0-9]+)";
         private const string CertificatePattern = @"(?<type>[-\w]+@openssh\.com)\s(?<data>[a-zA-Z0-9\/+=]*)(\s+(?<comment>.*))?";
 
 #if NET7_0_OR_GREATER
         private static readonly Regex PrivateKeyRegex = GetPrivateKeyRegex();
+        private static readonly Regex PuTTYPrivateKeyRegex = GetPrivateKeyPuTTYRegex();
         private static readonly Regex CertificateRegex = GetCertificateRegex();
 
         [GeneratedRegex(PrivateKeyPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)]
         private static partial Regex GetPrivateKeyRegex();
 
+        [GeneratedRegex(PuTTYPrivateKeyPattern, RegexOptions.Multiline | RegexOptions.ExplicitCapture)]
+        private static partial Regex GetPrivateKeyPuTTYRegex();
+
         [GeneratedRegex(CertificatePattern, RegexOptions.ExplicitCapture)]
         private static partial Regex GetCertificateRegex();
 #else
         private static readonly Regex PrivateKeyRegex = new Regex(PrivateKeyPattern, RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
+        private static readonly Regex PuTTYPrivateKeyRegex = new Regex(PuTTYPrivateKeyPattern, RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
         private static readonly Regex CertificateRegex = new Regex(CertificatePattern, RegexOptions.Compiled | RegexOptions.ExplicitCapture);
 #endif
 
@@ -287,7 +301,14 @@ namespace Renci.SshNet
             using (var sr = new StreamReader(privateKey))
             {
                 var text = sr.ReadToEnd();
-                privateKeyMatch = PrivateKeyRegex.Match(text);
+                if (text.StartsWith("PuTTY-User-Key-File", StringComparison.Ordinal))
+                {
+                    privateKeyMatch = PuTTYPrivateKeyRegex.Match(text);
+                }
+                else
+                {
+                    privateKeyMatch = PrivateKeyRegex.Match(text);
+                }
             }
 
             if (!privateKeyMatch.Success)
@@ -321,6 +342,34 @@ namespace Renci.SshNet
                 case "SSH2 ENCRYPTED PRIVATE KEY":
                     parser = new SSHCOM(binaryData, passPhrase);
                     break;
+                case "PuTTY-User-Key-File":
+                    var version = privateKeyMatch.Result("${version}");
+                    var algorithmName = privateKeyMatch.Result("${algorithmName}");
+                    var encryptionType = privateKeyMatch.Result("${encryptionType}");
+                    var comment = privateKeyMatch.Result("${comment}");
+                    var publicKey = privateKeyMatch.Result("${publicKey}");
+                    var argon2Type = privateKeyMatch.Result("${argon2Type}");
+                    var argon2Memory = privateKeyMatch.Result("${argon2Memory}");
+                    var argon2Passes = privateKeyMatch.Result("${argon2Passes}");
+                    var argon2Parallelism = privateKeyMatch.Result("${argon2Parallelism}");
+                    var argon2Salt = privateKeyMatch.Result("${argon2Salt}");
+                    var mac = privateKeyMatch.Result("${mac}");
+
+                    parser = new PuTTY(
+                        version,
+                        algorithmName,
+                        encryptionType,
+                        comment,
+                        Convert.FromBase64String(publicKey),
+                        argon2Type,
+                        argon2Salt,
+                        argon2Passes,
+                        argon2Memory,
+                        argon2Parallelism,
+                        binaryData,
+                        mac,
+                        passPhrase);
+                    break;
                 default:
                     throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", keyName));
             }

+ 9 - 0
test/Data/Key.PuTTY2.Ed25519.Encrypted.12345.ppk

@@ -0,0 +1,9 @@
+PuTTY-User-Key-File-2: ssh-ed25519
+Encryption: aes256-cbc
+Comment: Key.OPENSSH.ED25519
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol
+MGMi
+Private-Lines: 1
+f3N2/AkwbgVeXdK155h+JCbWgBXyEk3qEyx+ChUqm4tOUQGiJ95/mTo4RbIjWn+2
+Private-MAC: 823817b8364ce7f52e278e252fd10e2f51ac8554

+ 9 - 0
test/Data/Key.PuTTY2.Ed25519.ppk

@@ -0,0 +1,9 @@
+PuTTY-User-Key-File-2: ssh-ed25519
+Encryption: none
+Comment: Key.OPENSSH.ED25519
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol
+MGMi
+Private-Lines: 1
+AAAAIADMEXUw9TGuz7JykmHbzPOj8XebpZwo76iuxJtHkvAp
+Private-MAC: 6a1329739932b5caaaf48d7fcd61392d498b8f5f

+ 26 - 0
test/Data/Key.PuTTY2.RSA.Encrypted.12345.ppk

@@ -0,0 +1,26 @@
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: aes256-cbc
+Comment: Key.OPENSSH.RSA
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w
+oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m
+JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/
+G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi
+QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0
+4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09
+Private-Lines: 14
+i38C+M7U/qOvzvOdtEC60v3cn354TMdDpAVk22UuNgQY7u6avW907mUuXjqU7Tjy
+G66uQ+6vOnaQR6xTYWn3uI1YMWGsRylLiNZ/O/dIpITv+9xuU5U8qB+nFIH3iD6U
+cBuumRKH2k2IxYVBO1nKTt3T3HejIIy3JlFifs9ylahgPD1m8jIfNARnYB09fOQr
+g+nV4YNRntOqpf+cUpc+TGnz35oa9O+6fzJY/2hdkOJ+fUBdlYmcKEZZ9PgYHAlM
++aoQhL1vRoqZqzfHZYcMUBlmbRMgWiPfFB59nU0QbZ2uClbFxGGehBnCAsnd8Nni
+TafVYckp3kCNLmctrmorsRgM/IXWcdqmrGJO5sHEsutQqs9Dp3KM9xhRP23xoGnZ
+ssNw1qEAOP/k9JTCID/oeRE7RvAEMEdCs3l6FHKDvInR5xDuNrGehHdc3vGqklkC
+1kBER2vWRX+LANNXcfISRDNajC6XKsDo6aGJRPTMj/tbVJNbjP4Oi6fwcFizV9Z0
+yJiw1yV5AjUc8nG3cuTb1wNZ8DTPccq+W6ir8U+Vmdc67Mhxas6e2UKa5nXC+HqC
+jtM1oCPpxNyRoJSuxcWrwcs/yEhFfSOBU9PFsbJcX5i+ilZ2ZSipFc2fOcSx/6RC
+U36mELhmHPINoTDOc6qvL/fXRkqtoQGZm2UHoINVLkeMu3ERkxxDk9kFqRcSdFd+
+nSMJt9d3YRSsjhzvro65ugKiYBubugon24frB3nARI9zoe0BZB9H/RW3RPlMT8d9
+H+gElqyiYWZKKJs+Qht6NY0Hv1gp9erMIcTTts/w7ZRc5oS8pLCKOuB9wKYepJm7
+5I0x8qVotiHglDJ7PPbP+3UzrJvGp2nwPzj4qokCZKFhPYNYiCY16V0LaBRoE3mQ
+Private-MAC: 191817999553b67a7516223647e90e0b17db8918

+ 26 - 0
test/Data/Key.PuTTY2.RSA.ppk

@@ -0,0 +1,26 @@
+PuTTY-User-Key-File-2: ssh-rsa
+Encryption: none
+Comment: Key.OPENSSH.RSA
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w
+oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m
+JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/
+G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi
+QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0
+4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09
+Private-Lines: 14
+AAABAQDpeCr6CmnM632eu2zPkCN/W0eVJ6yftdpi4JFWA9veY5lK4RbcFR1NrRKv
+Z+TWfNIGlSt+qc3eJ3IraDdsPWxsFEOBQpH4Bo1wI3dOnF/GDJV4mFAu8SQR2i/N
+BFR/CtdF/GYTeOREZ9Vu/HKWsbynfnFyZfJ16XjqvaLx2PyAhje0qnREy9nhmU1u
+FYc93k7HIdYv17eBs5LIjKNCBMpl7OHMStL9f8on9dirPIECo2pnZGDWQqIdGUdL
+ooQja3IXBh+H5Fvov3FyHVKo61CFNaKubFLbl2kYPaOBqVd7KLDw+a6pOJYKpSZQ
+zHox0Xe0WyKuvngrhAD2Sox5pEu1AAAAgQD+dPDqesFjwMJ9SXwWbqkLY3H5yXje
+DZGEAXcm59L1buVHcqkkC2vIZQM0ToQPqib65bGYDPYfAsi08ropvJYpGR6HMDtd
+8wU3VWkPHNpSb39rl0yFzWR7HkuyE5HwYjtYUgeM/EQ5Dq9+Zhn3W8iSBQMBWReF
+7PFp0BfrxxGnawAAAIEA7t9vXgsFRX/YNMzR32bt9adFrRK3LEb+e36vlKD7aL/J
+8VBe9aDlnuSkhpxrTCAiN9ZAbT4VG73zprqja4CQY4I2z0JotMUgBOS90LhCkTY5
+WhN/1mnSgcM4SQ7WrrmJNYn5K3QFaeu18kOabsrhoFWkATT268QPYNSG8ni+P/cA
+AACBALFEE9FIau5dLoE3eGPfPWx+nltH6Jdtf5uwec5CUHqTWnVD07NfPLr7+Ip1
+vJ9jt0Qmp11h2XwidQLEfzBBFtgukA7b6ilx2831kJQmElcQdewo1ESmvHzWiAJP
+fM4JjTcDudzQZXsq1IT4L5t8bewAoKc12OUcDSS/P2tFjpoM
+Private-MAC: 7f487d19cb5d03257c9b9a2e1c7192a5d2396af5

+ 22 - 0
test/Data/Key.PuTTY3.DSA.Encrypted.Argon2id.12345.ppk

@@ -0,0 +1,22 @@
+PuTTY-User-Key-File-3: ssh-dss
+Encryption: aes256-cbc
+Comment: imported-openssh-key
+Public-Lines: 10
+AAAAB3NzaC1kc3MAAACBALVl3fae2O4qwsAK95SUShX0KMUNP+yl/uT3lGH9T/Zp
+tnHSlrTxnTWXCl0g91KEeCaEnDDhLxm4aCv1Ag4B/yvcM4u34qkmaNLy2LiAxiqd
+obZcNG61Pqwqd5IDkp38LBsn8tmb12xu9NalpUfOiSEB1cyCr4zFZMrm0wtdyJQV
+AAAAFQCu+iNkqf/YOAYjYrHSCHFmWAfEYQAAAIAOVJ434UAR3Hn6lA5nWNfFOuUV
+H3W7nJaP0FQJiIPx7GUbdxO9qtDNTbWkWL3c9qx5+B7Ole4xM7cvyXPrNQUYDHCF
+lS+Ue2x3IeJrkdfZkH9ePP25y5A0J4/c+8XXvQaj4zA5nfw13oy5Ptyd7d3Kq5tE
+DM8KiVdIhwkXjUA3PQAAAIEAm8IGZQatS7M6AfNITNWG4TI7Z2aRQjLb9/MWJIID
+7c/VQ4zdTZdG3kpk0Gj9n4xreopK5NmYAdj8rtFfPBgmXltsLqt+bBcXkpxW//7W
+C29WOXW3t90ySTh+cWuWfr9fV7mf4Ql/6u/ZIgpQNvnNYezazt3fK8EXjI1dAXEu
+QxE=
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 8
+Argon2-Parallelism: 1
+Argon2-Salt: 310d916da49faba22ba8d2745777e5c5
+Private-Lines: 1
+xMm0Tg+o7Yq6lAs6L33y2fy3fiDPl6p71iKxm8OAgj4=
+Private-MAC: fc2aef48bf90b80b97d06c32c37491db614331c2551ab37865d8719ee6cb5f4f

+ 17 - 0
test/Data/Key.PuTTY3.DSA.ppk

@@ -0,0 +1,17 @@
+PuTTY-User-Key-File-3: ssh-dss
+Encryption: none
+Comment: imported-openssh-key
+Public-Lines: 10
+AAAAB3NzaC1kc3MAAACBALVl3fae2O4qwsAK95SUShX0KMUNP+yl/uT3lGH9T/Zp
+tnHSlrTxnTWXCl0g91KEeCaEnDDhLxm4aCv1Ag4B/yvcM4u34qkmaNLy2LiAxiqd
+obZcNG61Pqwqd5IDkp38LBsn8tmb12xu9NalpUfOiSEB1cyCr4zFZMrm0wtdyJQV
+AAAAFQCu+iNkqf/YOAYjYrHSCHFmWAfEYQAAAIAOVJ434UAR3Hn6lA5nWNfFOuUV
+H3W7nJaP0FQJiIPx7GUbdxO9qtDNTbWkWL3c9qx5+B7Ole4xM7cvyXPrNQUYDHCF
+lS+Ue2x3IeJrkdfZkH9ePP25y5A0J4/c+8XXvQaj4zA5nfw13oy5Ptyd7d3Kq5tE
+DM8KiVdIhwkXjUA3PQAAAIEAm8IGZQatS7M6AfNITNWG4TI7Z2aRQjLb9/MWJIID
+7c/VQ4zdTZdG3kpk0Gj9n4xreopK5NmYAdj8rtFfPBgmXltsLqt+bBcXkpxW//7W
+C29WOXW3t90ySTh+cWuWfr9fV7mf4Ql/6u/ZIgpQNvnNYezazt3fK8EXjI1dAXEu
+QxE=
+Private-Lines: 1
+AAAAFBhGOzk+Aimeob964E8+HsQNlyde
+Private-MAC: 6c517ac5ede72c006b0115dd9d0830c8e699a1f4d72c708d41f68b6263d974ae

+ 15 - 0
test/Data/Key.PuTTY3.ECDSA.Encrypted.Argon2id.12345.ppk

@@ -0,0 +1,15 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp256
+Encryption: aes256-cbc
+Comment: imported-openssh-key
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEA+TDv5/cqk
+g07M8M1aQKS8eUkBXnBOWXw5IMalXR0HnJtQQD6M2eHihjYSp+9oU+/Zi5afR11/
+qDRHLlU/Nx8=
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 8
+Argon2-Parallelism: 1
+Argon2-Salt: d496ed94c393d7c5df6ecd10440210ba
+Private-Lines: 1
+7gcJBC98J9hOJVkp0rUJrx8vlWIklCf+/7iQZT+1pCyhaWs+5jm/dIw8aCGMhM0a
+Private-MAC: 916979ee4696a5ee7674816259b2ac32c5258eaec94c554a9bc8d090dbc45b50

+ 10 - 0
test/Data/Key.PuTTY3.ECDSA.ppk

@@ -0,0 +1,10 @@
+PuTTY-User-Key-File-3: ecdsa-sha2-nistp256
+Encryption: none
+Comment: imported-openssh-key
+Public-Lines: 3
+AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBEA+TDv5/cqk
+g07M8M1aQKS8eUkBXnBOWXw5IMalXR0HnJtQQD6M2eHihjYSp+9oU+/Zi5afR11/
+qDRHLlU/Nx8=
+Private-Lines: 1
+AAAAIEdqaFKgJBIibVjyUh1v7Y35LwIQJrocdTaYFLwl7iB0
+Private-MAC: 3df7a232c8e3021a8f9809968b748c9db46ad3c668fa7d3f4d87ec8cbd838522

+ 14 - 0
test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2d.12345.ppk

@@ -0,0 +1,14 @@
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: aes256-cbc
+Comment: Key.OPENSSH.ED25519
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol
+MGMi
+Key-Derivation: Argon2d
+Argon2-Memory: 8192
+Argon2-Passes: 8
+Argon2-Parallelism: 1
+Argon2-Salt: a937df822b7304499c58f1795e90376a
+Private-Lines: 1
+EjcQkrXqEA3uJb/TfLt8MM+Gt1pM80S5fn5AlUWiUdhvrCG7pTTSVpaYYP8svW89
+Private-MAC: 673bd514209afa19d14506300f236a6049a915e7c7ec1a67d8df464e948f2654

+ 14 - 0
test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2i.12345.ppk

@@ -0,0 +1,14 @@
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: aes256-cbc
+Comment: Key.OPENSSH.ED25519
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol
+MGMi
+Key-Derivation: Argon2i
+Argon2-Memory: 8192
+Argon2-Passes: 8
+Argon2-Parallelism: 1
+Argon2-Salt: bf99b3e5c7d28d566c8c759d8a1a41d2
+Private-Lines: 1
+tbSkuSPLzdnnQuGYnC/L1fv9UnFzCBkwtk5aLDZYJFiD08h5EmpGCfJJHe0lJx9/
+Private-MAC: eee47ba955ba32fc85e3eb7a70d7e5f0359eacbf9f1abd5f02904fa98db513b7

+ 14 - 0
test/Data/Key.PuTTY3.Ed25519.Encrypted.Argon2id.12345.ppk

@@ -0,0 +1,14 @@
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: aes256-cbc
+Comment: Key.OPENSSH.ED25519
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol
+MGMi
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 21
+Argon2-Parallelism: 1
+Argon2-Salt: a8bb359d93fc58732e595da9c80b2e88
+Private-Lines: 1
+GHfMNhx9V3k/OKJxNb6QJ93Gwg9/slksETZ0Ns0edelE6hJfUc+XhXnQBLkUHZ6H
+Private-MAC: b07d3ddb544a7396637bc607bedd3d14ed28db929f2bdb5af4a94b410c3d9feb

+ 9 - 0
test/Data/Key.PuTTY3.Ed25519.ppk

@@ -0,0 +1,9 @@
+PuTTY-User-Key-File-3: ssh-ed25519
+Encryption: none
+Comment: Key.OPENSSH.ED25519
+Public-Lines: 2
+AAAAC3NzaC1lZDI1NTE5AAAAIA0JZnDQrxQZcNALfZYG7LPAW1MYEGvVW5nje7Ol
+MGMi
+Private-Lines: 1
+AAAAIADMEXUw9TGuz7JykmHbzPOj8XebpZwo76iuxJtHkvAp
+Private-MAC: 273204de25dcb0fc8835d6d08b8045be34358bf64d009a7566d97851449fb9ab

+ 31 - 0
test/Data/Key.PuTTY3.RSA.Encrypted.Argon2id.12345.ppk

@@ -0,0 +1,31 @@
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: aes256-cbc
+Comment: Key.OPENSSH.RSA
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w
+oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m
+JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/
+G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi
+QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0
+4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09
+Key-Derivation: Argon2id
+Argon2-Memory: 8192
+Argon2-Passes: 13
+Argon2-Parallelism: 1
+Argon2-Salt: 433d83be99e63cd358ef5df037448734
+Private-Lines: 14
+q3I+FoI3y6nzGIi1lIXMIx4J/yOlIYIqoHQbpQxFiUwoDxYYm8zy4BYexEw/Ox6n
+LaKo4LKyvh36+5lrjqMluWsJ+PXrA+GBxSXsanzWG42RPRNbNF7RQCqTrxDRgTSv
+GztaXcS5URSIutOl5vePib/9lFC5FlYRp+dCM0CVRpkUezgbZdnOQ5901DhylceT
+fUXzpUcEHI4i1kp9awG+4ZpfJf057HuHFhmIvNECirt//LKBH9vrr7NQ7t1Sv8+r
+NdzkcUHpCyvPLqII/zaQurgFlrYTCIyqzKHphoVZGa2XrB6VHgt/pvh+a0fOw/lB
+c3t377cOr1PTht7AhYoeUPheFaYE9bPyZ5pV0Ai5IlOD94blp94BWU56e8GNx8Gp
+SB0zCqrW80Q39OXb3hweyIcVEbb8dfY6sJf3PhzEL1pehUzOFSRlwjGl7BsYZCe3
+uIUyNzczpbvk6OTReGRHM7DX5xz3t/esC7VXP7CpOnjhdr9dZ6dpqGz/vDciyxV9
+Jw/gaipPAQXUf6ls3atooxN1IpLnwtPE1FHAzofV9ixW4TqHmv1H9++94VJKFyA0
+nPUVBtKhM8gT/4ZxnPEDc/sl/EYb9DmuaUC8cTV2O10Wpnj3HhS1Qh43qAguyOu/
+qF7QGETSweXTYSgresMxeLRkX/tia6tz2Rfk1HnP8sRbMyIOyOvU4pNMDuTelGy9
+7OfsS54bcBIM1DTpMZ7HiKctZ6DvjL4VtgIJPlXmc+s4CD6cjbycOHo5iQfjsoS4
+Wh2dHGK3bYVJ+LTA/Z63nasR9CLW26oL76CI/+TXvheyFotzIjtn3iKpJUqS7Dc0
+vpZT3x5OWxmYlu1xZYbzMkLIedE8WaKYjeb3qE3Ox+rN0xIYXoNiCi6jxU+viuF4
+Private-MAC: 3bf7e6fe566fab36eba63d3129929b197f6b07e234ebcf22549d10b1d97e3e2f

+ 26 - 0
test/Data/Key.PuTTY3.RSA.ppk

@@ -0,0 +1,26 @@
+PuTTY-User-Key-File-3: ssh-rsa
+Encryption: none
+Comment: Key.OPENSSH.RSA
+Public-Lines: 6
+AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w
+oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m
+JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/
+G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi
+QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0
+4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09
+Private-Lines: 14
+AAABAQDpeCr6CmnM632eu2zPkCN/W0eVJ6yftdpi4JFWA9veY5lK4RbcFR1NrRKv
+Z+TWfNIGlSt+qc3eJ3IraDdsPWxsFEOBQpH4Bo1wI3dOnF/GDJV4mFAu8SQR2i/N
+BFR/CtdF/GYTeOREZ9Vu/HKWsbynfnFyZfJ16XjqvaLx2PyAhje0qnREy9nhmU1u
+FYc93k7HIdYv17eBs5LIjKNCBMpl7OHMStL9f8on9dirPIECo2pnZGDWQqIdGUdL
+ooQja3IXBh+H5Fvov3FyHVKo61CFNaKubFLbl2kYPaOBqVd7KLDw+a6pOJYKpSZQ
+zHox0Xe0WyKuvngrhAD2Sox5pEu1AAAAgQD+dPDqesFjwMJ9SXwWbqkLY3H5yXje
+DZGEAXcm59L1buVHcqkkC2vIZQM0ToQPqib65bGYDPYfAsi08ropvJYpGR6HMDtd
+8wU3VWkPHNpSb39rl0yFzWR7HkuyE5HwYjtYUgeM/EQ5Dq9+Zhn3W8iSBQMBWReF
+7PFp0BfrxxGnawAAAIEA7t9vXgsFRX/YNMzR32bt9adFrRK3LEb+e36vlKD7aL/J
+8VBe9aDlnuSkhpxrTCAiN9ZAbT4VG73zprqja4CQY4I2z0JotMUgBOS90LhCkTY5
+WhN/1mnSgcM4SQ7WrrmJNYn5K3QFaeu18kOabsrhoFWkATT268QPYNSG8ni+P/cA
+AACBALFEE9FIau5dLoE3eGPfPWx+nltH6Jdtf5uwec5CUHqTWnVD07NfPLr7+Ip1
+vJ9jt0Qmp11h2XwidQLEfzBBFtgukA7b6ilx2831kJQmElcQdewo1ESmvHzWiAJP
+fM4JjTcDudzQZXsq1IT4L5t8bewAoKc12OUcDSS/P2tFjpoM
+Private-MAC: ef76b1cf66a4a28d6fe08c70012c4bfa61771502e496d227dd77580650d20bfd

+ 94 - 0
test/Renci.SshNet.Tests/Classes/PrivateKeyFileTest.cs

@@ -344,6 +344,20 @@ namespace Renci.SshNet.Tests.Classes
         [DataRow("Key.OPENSSH.RSA.Encrypted.Aes.192.CTR.txt", "12345", typeof(RsaKey))]
         [DataRow("Key.OPENSSH.RSA.Encrypted.txt", "12345", typeof(RsaKey))]
         [DataRow("Key.OPENSSH.RSA.txt", null, typeof(RsaKey))]
+        [DataRow("Key.PuTTY2.Ed25519.Encrypted.12345.ppk", "12345", typeof(ED25519Key))]
+        [DataRow("Key.PuTTY2.Ed25519.ppk", null, typeof(ED25519Key))]
+        [DataRow("Key.PuTTY2.RSA.Encrypted.12345.ppk", "12345", typeof(RsaKey))]
+        [DataRow("Key.PuTTY2.RSA.ppk", null, typeof(RsaKey))]
+        [DataRow("Key.PuTTY3.DSA.Encrypted.Argon2id.12345.ppk", "12345", typeof(DsaKey))]
+        [DataRow("Key.PuTTY3.DSA.ppk", null, typeof(DsaKey))]
+        [DataRow("Key.PuTTY3.ECDSA.Encrypted.Argon2id.12345.ppk", "12345", typeof(EcdsaKey))]
+        [DataRow("Key.PuTTY3.ECDSA.ppk", null, typeof(EcdsaKey))]
+        [DataRow("Key.PuTTY3.Ed25519.Encrypted.Argon2i.12345.ppk", "12345", typeof(ED25519Key))]
+        [DataRow("Key.PuTTY3.Ed25519.Encrypted.Argon2d.12345.ppk", "12345", typeof(ED25519Key))]
+        [DataRow("Key.PuTTY3.Ed25519.Encrypted.Argon2id.12345.ppk", "12345", typeof(ED25519Key))]
+        [DataRow("Key.PuTTY3.Ed25519.ppk", null, typeof(ED25519Key))]
+        [DataRow("Key.PuTTY3.RSA.Encrypted.Argon2id.12345.ppk", "12345", typeof(RsaKey))]
+        [DataRow("Key.PuTTY3.RSA.ppk", null, typeof(RsaKey))]
         [DataRow("Key.RSA.Encrypted.Aes.128.CBC.12345.txt", "12345", typeof(RsaKey))]
         [DataRow("Key.RSA.Encrypted.Aes.192.CBC.12345.txt", "12345", typeof(RsaKey))]
         [DataRow("Key.RSA.Encrypted.Aes.256.CBC.12345.txt", "12345", typeof(RsaKey))]
@@ -523,6 +537,86 @@ namespace Renci.SshNet.Tests.Classes
             }
         }
 
+        [TestMethod]
+        public void PuTTYv2_InvalidMac_ThrowsSshException()
+        {
+            string pk = """
+            PuTTY-User-Key-File-2: ssh-rsa
+            Encryption: none
+            Comment: Key.OPENSSH.RSA
+            Public-Lines: 6
+            AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w
+            oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m
+            JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/
+            G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi
+            QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0
+            4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09
+            Private-Lines: 14
+            AAABAQDpeCr6CmnM632eu2zPkCN/W0eVJ6yftdpi4JFWA9veY5lK4RbcFR1NrRKv
+            Z+TWfNIGlSt+qc3eJ3IraDdsPWxsFEOBQpH4Bo1wI3dOnF/GDJV4mFAu8SQR2i/N
+            BFR/CtdF/GYTeOREZ9Vu/HKWsbynfnFyZfJ16XjqvaLx2PyAhje0qnREy9nhmU1u
+            FYc93k7HIdYv17eBs5LIjKNCBMpl7OHMStL9f8on9dirPIECo2pnZGDWQqIdGUdL
+            ooQja3IXBh+H5Fvov3FyHVKo61CFNaKubFLbl2kYPaOBqVd7KLDw+a6pOJYKpSZQ
+            zHox0Xe0WyKuvngrhAD2Sox5pEu1AAAAgQD+dPDqesFjwMJ9SXwWbqkLY3H5yXje
+            DZGEAXcm59L1buVHcqkkC2vIZQM0ToQPqib65bGYDPYfAsi08ropvJYpGR6HMDtd
+            8wU3VWkPHNpSb39rl0yFzWR7HkuyE5HwYjtYUgeM/EQ5Dq9+Zhn3W8iSBQMBWReF
+            7PFp0BfrxxGnawAAAIEA7t9vXgsFRX/YNMzR32bt9adFrRK3LEb+e36vlKD7aL/J
+            8VBe9aDlnuSkhpxrTCAiN9ZAbT4VG73zprqja4CQY4I2z0JotMUgBOS90LhCkTY5
+            WhN/1mnSgcM4SQ7WrrmJNYn5K3QFaeu18kOabsrhoFWkATT268QPYNSG8ni+P/cA
+            AACBALFEE9FIau5dLoE3eGPfPWx+nltH6Jdtf5uwec5CUHqTWnVD07NfPLr7+Ip1
+            vJ9jt0Qmp11h2XwidQLEfzBBFtgukA7b6ilx2831kJQmElcQdewo1ESmvHzWiAJP
+            fM4JjTcDudzQZXsq1IT4L5t8bewAoKc12OUcDSS/P2tFjpoM
+            Private-MAC: 7f487d19cb5d03257c9b9a2aaaaaaaaaaaaaaaaa
+            """;
+
+            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(pk)))
+            {
+                var ex = Assert.ThrowsException<SshException>(() => new PrivateKeyFile(stream));
+
+                Assert.AreEqual("MAC verification failed for PuTTY key file", ex.Message);
+            }
+        }
+
+        [TestMethod]
+        public void PuTTYv3_InvalidMac_ThrowsSshException()
+        {
+            string pk = """
+            PuTTY-User-Key-File-3: ssh-rsa
+            Encryption: none
+            Comment: Key.OPENSSH.RSA
+            Public-Lines: 6
+            AAAAB3NzaC1yc2EAAAADAQABAAABAQDtbs6KCLsePWaxraXweKYs/NqBWYT8Kx4w
+            oJHE8xO1ZO+hl0y3uF+S2FYDuHbRruhJJ4fa3sWp46lU0YVi9FXcFVawpkkxFx0m
+            JMJkCMffytiT3Re9neYqso3/d9xCyHg6I+dapPodKqDXiiJXxQ+1TCcTrmyRZLG/
+            G34QuVWkKobm8TY78Y0MpATsXNi3q9CKEwVIAEGqO9q7SaNfTTYpiIIyvq+CXxdi
+            QMDifn4nJBJDHOed+sv3dmhqq6NE/ZtPlSFeBvOvwcXC6pAa9REQJlNMjwGK//q0
+            4if3HaERo3q/EMu1dz30TZ3o1bpx2uLBoYUniOBVYMTmZTTTpd09
+            Private-Lines: 14
+            AAABAQDpeCr6CmnM632eu2zPkCN/W0eVJ6yftdpi4JFWA9veY5lK4RbcFR1NrRKv
+            Z+TWfNIGlSt+qc3eJ3IraDdsPWxsFEOBQpH4Bo1wI3dOnF/GDJV4mFAu8SQR2i/N
+            BFR/CtdF/GYTeOREZ9Vu/HKWsbynfnFyZfJ16XjqvaLx2PyAhje0qnREy9nhmU1u
+            FYc93k7HIdYv17eBs5LIjKNCBMpl7OHMStL9f8on9dirPIECo2pnZGDWQqIdGUdL
+            ooQja3IXBh+H5Fvov3FyHVKo61CFNaKubFLbl2kYPaOBqVd7KLDw+a6pOJYKpSZQ
+            zHox0Xe0WyKuvngrhAD2Sox5pEu1AAAAgQD+dPDqesFjwMJ9SXwWbqkLY3H5yXje
+            DZGEAXcm59L1buVHcqkkC2vIZQM0ToQPqib65bGYDPYfAsi08ropvJYpGR6HMDtd
+            8wU3VWkPHNpSb39rl0yFzWR7HkuyE5HwYjtYUgeM/EQ5Dq9+Zhn3W8iSBQMBWReF
+            7PFp0BfrxxGnawAAAIEA7t9vXgsFRX/YNMzR32bt9adFrRK3LEb+e36vlKD7aL/J
+            8VBe9aDlnuSkhpxrTCAiN9ZAbT4VG73zprqja4CQY4I2z0JotMUgBOS90LhCkTY5
+            WhN/1mnSgcM4SQ7WrrmJNYn5K3QFaeu18kOabsrhoFWkATT268QPYNSG8ni+P/cA
+            AACBALFEE9FIau5dLoE3eGPfPWx+nltH6Jdtf5uwec5CUHqTWnVD07NfPLr7+Ip1
+            vJ9jt0Qmp11h2XwidQLEfzBBFtgukA7b6ilx2831kJQmElcQdewo1ESmvHzWiAJP
+            fM4JjTcDudzQZXsq1IT4L5t8bewAoKc12OUcDSS/P2tFjpoM
+            Private-MAC: ef76b1cf66a4a28d6fe08c70012c4bfa61771502e496d227dddddddddddddddd
+            """;
+
+            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(pk)))
+            {
+                var ex = Assert.ThrowsException<SshException>(() => new PrivateKeyFile(stream));
+
+                Assert.AreEqual("MAC verification failed for PuTTY key file", ex.Message);
+            }
+        }
+
         private void SaveStreamToFile(Stream stream, string fileName)
         {
             var buffer = new byte[4000];