Prechádzať zdrojové kódy

Split PrivateKeyFile into different implementations. (#1542)

* Split PrivateKeyFile into different implementations.

* Remove duplicate keyName check. Get cipherName and salt only if the key is PKCS1 format.

---------

Co-authored-by: Rob Hague <rob.hague00@gmail.com>
Scott Xu 11 mesiacov pred
rodič
commit
3ec45e1af3

+ 249 - 0
src/Renci.SshNet/PrivateKeyFile.OpenSSH.cs

@@ -0,0 +1,249 @@
+#nullable enable
+using System;
+using System.Globalization;
+using System.Linq;
+using System.Text;
+
+using Renci.SshNet.Common;
+using Renci.SshNet.Security;
+using Renci.SshNet.Security.Cryptography;
+using Renci.SshNet.Security.Cryptography.Ciphers;
+using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
+
+namespace Renci.SshNet
+{
+    public partial class PrivateKeyFile
+    {
+        private sealed class OpenSSH : IPrivateKeyParser
+        {
+            private readonly byte[] _data;
+            private readonly string? _passPhrase;
+
+            public OpenSSH(byte[] data, string? passPhrase)
+            {
+                _data = data;
+                _passPhrase = passPhrase;
+            }
+
+            /// <summary>
+            /// Parses an OpenSSH V1 key file according to the key spec:
+            /// <see href="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key"/>.
+            /// </summary>
+            public Key Parse()
+            {
+                var keyReader = new SshDataReader(_data);
+
+                // check magic header
+                var authMagic = "openssh-key-v1\0"u8;
+                var keyHeaderBytes = keyReader.ReadBytes(authMagic.Length);
+                if (!authMagic.SequenceEqual(keyHeaderBytes))
+                {
+                    throw new SshException("This openssh key does not contain the 'openssh-key-v1' format magic header");
+                }
+
+                // cipher will be "aes256-cbc" or other cipher if using a passphrase, "none" otherwise
+                var cipherName = keyReader.ReadString(Encoding.UTF8);
+
+                // key derivation function (kdf): bcrypt or nothing
+                var kdfName = keyReader.ReadString(Encoding.UTF8);
+
+                // kdf options length: 24 if passphrase, 0 if no passphrase
+                var kdfOptionsLen = (int)keyReader.ReadUInt32();
+                byte[]? salt = null;
+                var rounds = 0;
+                if (kdfOptionsLen > 0)
+                {
+                    var saltLength = (int)keyReader.ReadUInt32();
+                    salt = keyReader.ReadBytes(saltLength);
+                    rounds = (int)keyReader.ReadUInt32();
+                }
+
+                // number of public keys, only supporting 1 for now
+                var numberOfPublicKeys = (int)keyReader.ReadUInt32();
+                if (numberOfPublicKeys != 1)
+                {
+                    throw new SshException("At this time only one public key in the openssh key is supported.");
+                }
+
+                // read public key in ssh-format, but we dont need it
+                _ = keyReader.ReadString(Encoding.UTF8);
+
+                // possibly encrypted private key
+                var privateKeyLength = (int)keyReader.ReadUInt32();
+                byte[] privateKeyBytes;
+
+                // decrypt private key if necessary
+                if (cipherName != "none")
+                {
+                    if (string.IsNullOrEmpty(_passPhrase))
+                    {
+                        throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
+                    }
+
+                    if (string.IsNullOrEmpty(kdfName) || kdfName != "bcrypt")
+                    {
+                        throw new SshException("kdf " + kdfName + " is not supported for openssh key file");
+                    }
+
+                    var ivLength = 16;
+                    CipherInfo cipherInfo;
+                    switch (cipherName)
+                    {
+                        case "3des-cbc":
+                            ivLength = 8;
+                            cipherInfo = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null));
+                            break;
+                        case "aes128-cbc":
+                            cipherInfo = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
+                            break;
+                        case "aes192-cbc":
+                            cipherInfo = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
+                            break;
+                        case "aes256-cbc":
+                            cipherInfo = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
+                            break;
+                        case "aes128-ctr":
+                            cipherInfo = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
+                            break;
+                        case "aes192-ctr":
+                            cipherInfo = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
+                            break;
+                        case "aes256-ctr":
+                            cipherInfo = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
+                            break;
+                        case "aes128-gcm@openssh.com":
+                            cipherInfo = new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv, aadLength: 0), isAead: true);
+                            break;
+                        case "aes256-gcm@openssh.com":
+                            cipherInfo = new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv, aadLength: 0), isAead: true);
+                            break;
+                        case "chacha20-poly1305@openssh.com":
+                            ivLength = 12;
+                            cipherInfo = new CipherInfo(256, (key, iv) => new ChaCha20Poly1305Cipher(key, aadLength: 0), isAead: true);
+                            break;
+                        default:
+                            throw new SshException("Cipher '" + cipherName + "' is not supported for an OpenSSH key.");
+                    }
+
+                    var keyLength = cipherInfo.KeySize / 8;
+
+                    // inspired by the SSHj library (https://github.com/hierynomus/sshj)
+                    // apply the kdf to derive a key and iv from the passphrase
+                    var passPhraseBytes = Encoding.UTF8.GetBytes(_passPhrase);
+                    var keyiv = new byte[keyLength + ivLength];
+                    new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv);
+
+                    var key = keyiv.Take(keyLength);
+                    var iv = keyiv.Take(keyLength, ivLength);
+
+                    var cipher = cipherInfo.Cipher(key, iv);
+
+                    // The authentication tag data (if any) is concatenated to the end of the encrypted private key string.
+                    // See https://github.com/openssh/openssh-portable/blob/509b757c052ea969b3a41fc36818b44801caf1cf/sshkey.c#L2951
+                    // and https://github.com/openssh/openssh-portable/blob/509b757c052ea969b3a41fc36818b44801caf1cf/cipher.c#L340
+                    var cipherData = keyReader.ReadBytes(privateKeyLength + cipher.TagSize);
+
+                    try
+                    {
+                        privateKeyBytes = cipher.Decrypt(cipherData, 0, privateKeyLength);
+                    }
+                    finally
+                    {
+                        if (cipher is IDisposable disposable)
+                        {
+                            disposable.Dispose();
+                        }
+                    }
+                }
+                else
+                {
+                    privateKeyBytes = keyReader.ReadBytes(privateKeyLength);
+                }
+
+                // validate private key length
+                privateKeyLength = privateKeyBytes.Length;
+                if (privateKeyLength % 8 != 0)
+                {
+                    throw new SshException("The private key section must be a multiple of the block size (8)");
+                }
+
+                // now parse the data we called the private key, it actually contains the public key again
+                // so we need to parse through it to get the private key bytes, plus there's some
+                // validation we need to do.
+                var privateKeyReader = new SshDataReader(privateKeyBytes);
+
+                // check ints should match, they wouldn't match for example if the wrong passphrase was supplied
+                var checkInt1 = (int)privateKeyReader.ReadUInt32();
+                var checkInt2 = (int)privateKeyReader.ReadUInt32();
+                if (checkInt1 != checkInt2)
+                {
+                    throw new SshException(string.Format(CultureInfo.InvariantCulture,
+                                                         "The random check bytes of the OpenSSH key do not match ({0} <-> {1}).",
+                                                         checkInt1.ToString(CultureInfo.InvariantCulture),
+                                                         checkInt2.ToString(CultureInfo.InvariantCulture)));
+                }
+
+                // key type
+                var keyType = privateKeyReader.ReadString(Encoding.UTF8);
+
+                Key parsedKey;
+                byte[] publicKey;
+                byte[] unencryptedPrivateKey;
+                switch (keyType)
+                {
+                    case "ssh-ed25519":
+                        // https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-11#section-3.2.3
+
+                        // ENC(A)
+                        _ = privateKeyReader.ReadBignum2();
+
+                        // k || ENC(A)
+                        unencryptedPrivateKey = privateKeyReader.ReadBignum2();
+                        parsedKey = new ED25519Key(unencryptedPrivateKey);
+                        break;
+                    case "ecdsa-sha2-nistp256":
+                    case "ecdsa-sha2-nistp384":
+                    case "ecdsa-sha2-nistp521":
+                        // curve
+                        var len = (int)privateKeyReader.ReadUInt32();
+                        var curve = Encoding.ASCII.GetString(privateKeyReader.ReadBytes(len));
+
+                        // public key
+                        publicKey = privateKeyReader.ReadBignum2();
+
+                        // private key
+                        unencryptedPrivateKey = privateKeyReader.ReadBignum2();
+                        parsedKey = new EcdsaKey(curve, publicKey, unencryptedPrivateKey.TrimLeadingZeros());
+                        break;
+                    case "ssh-rsa":
+                        var modulus = privateKeyReader.ReadBignum(); // n
+                        var exponent = privateKeyReader.ReadBignum(); // e
+                        var d = privateKeyReader.ReadBignum(); // d
+                        var inverseQ = privateKeyReader.ReadBignum(); // iqmp
+                        var p = privateKeyReader.ReadBignum(); // p
+                        var q = privateKeyReader.ReadBignum(); // q
+                        parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
+                        break;
+                    default:
+                        throw new SshException("OpenSSH key type '" + keyType + "' is not supported.");
+                }
+
+                parsedKey.Comment = privateKeyReader.ReadString(Encoding.UTF8);
+
+                // The list of privatekey/comment pairs is padded with the bytes 1, 2, 3, ...
+                // until the total length is a multiple of the cipher block size.
+                var padding = privateKeyReader.ReadBytes();
+                for (var i = 0; i < padding.Length; i++)
+                {
+                    if ((int)padding[i] != i + 1)
+                    {
+                        throw new SshException("Padding of openssh key format contained wrong byte at position: " +
+                                               i.ToString(CultureInfo.InvariantCulture));
+                    }
+                }
+
+                return parsedKey;
+            }
+        }
+    }
+}

+ 150 - 0
src/Renci.SshNet/PrivateKeyFile.PKCS1.cs

@@ -0,0 +1,150 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.Security.Cryptography;
+using System.Text;
+
+using Renci.SshNet.Common;
+using Renci.SshNet.Security;
+using Renci.SshNet.Security.Cryptography.Ciphers;
+using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
+using Renci.SshNet.Security.Cryptography.Ciphers.Paddings;
+
+namespace Renci.SshNet
+{
+    public partial class PrivateKeyFile
+    {
+        private sealed class PKCS1 : IPrivateKeyParser
+        {
+            private readonly string _cipherName;
+            private readonly string _salt;
+            private readonly string _keyName;
+            private readonly byte[] _data;
+            private readonly string? _passPhrase;
+
+            public PKCS1(string cipherName, string salt, string keyName, byte[] data, string? passPhrase)
+            {
+                _cipherName = cipherName;
+                _salt = salt;
+                _keyName = keyName;
+                _data = data;
+                _passPhrase = passPhrase;
+            }
+
+            public Key Parse()
+            {
+                byte[] decryptedData;
+                if (!string.IsNullOrEmpty(_cipherName) && !string.IsNullOrEmpty(_salt))
+                {
+                    if (string.IsNullOrEmpty(_passPhrase))
+                    {
+                        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);
+                    }
+
+                    CipherInfo cipher;
+                    switch (_cipherName)
+                    {
+                        case "DES-EDE3-CBC":
+                            cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
+                            break;
+                        case "DES-EDE3-CFB":
+                            cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CfbCipherMode(iv), new PKCS7Padding()));
+                            break;
+                        case "DES-CBC":
+                            cipher = new CipherInfo(64, (key, iv) => new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
+                            break;
+                        case "AES-128-CBC":
+                            cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
+                            break;
+                        case "AES-192-CBC":
+                            cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
+                            break;
+                        case "AES-256-CBC":
+                            cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
+                            break;
+                        default:
+                            throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key cipher \"{0}\" is not supported.", _cipherName));
+                    }
+
+                    decryptedData = DecryptKey(cipher, _data, _passPhrase, binarySalt);
+                }
+                else
+                {
+                    decryptedData = _data;
+                }
+
+                switch (_keyName)
+                {
+                    case "RSA PRIVATE KEY":
+                        return new RsaKey(decryptedData);
+                    case "DSA PRIVATE KEY":
+                        return new DsaKey(decryptedData);
+                    case "EC PRIVATE KEY":
+                        return new EcdsaKey(decryptedData);
+                    default:
+                        throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", _keyName));
+                }
+            }
+
+            /// <summary>
+            /// Decrypts encrypted private key file data.
+            /// </summary>
+            /// <param name="cipherInfo">The cipher info.</param>
+            /// <param name="cipherData">Encrypted data.</param>
+            /// <param name="passPhrase">Decryption pass phrase.</param>
+            /// <param name="binarySalt">Decryption binary salt.</param>
+            /// <returns>Decrypted byte array.</returns>
+            /// <exception cref="ArgumentNullException"><paramref name="cipherInfo" />, <paramref name="cipherData" />, <paramref name="passPhrase" /> or <paramref name="binarySalt" /> is <see langword="null"/>.</exception>
+            private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt)
+            {
+                Debug.Assert(cipherInfo != null);
+                Debug.Assert(cipherData != null);
+                Debug.Assert(binarySalt != null);
+
+                var cipherKey = new List<byte>();
+
+#pragma warning disable CA1850 // Prefer static HashData method; We'll reuse the object on lower targets.
+                using (var md5 = MD5.Create())
+                {
+                    var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
+
+                    // Use 8 bytes binary salt
+                    var initVector = passwordBytes.Concat(binarySalt.Take(8));
+
+                    var hash = md5.ComputeHash(initVector);
+                    cipherKey.AddRange(hash);
+
+                    while (cipherKey.Count < cipherInfo.KeySize / 8)
+                    {
+                        hash = hash.Concat(initVector);
+                        hash = md5.ComputeHash(hash);
+                        cipherKey.AddRange(hash);
+                    }
+                }
+#pragma warning restore CA1850 // Prefer static HashData method
+
+                var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
+
+                try
+                {
+                    return cipher.Decrypt(cipherData);
+                }
+                finally
+                {
+                    if (cipher is IDisposable disposable)
+                    {
+                        disposable.Dispose();
+                    }
+                }
+            }
+        }
+    }
+}

+ 115 - 0
src/Renci.SshNet/PrivateKeyFile.PKCS8.cs

@@ -0,0 +1,115 @@
+#nullable enable
+using System;
+using System.Formats.Asn1;
+using System.Globalization;
+using System.Numerics;
+
+using Org.BouncyCastle.Asn1.EdEC;
+using Org.BouncyCastle.Asn1.Pkcs;
+using Org.BouncyCastle.Asn1.X9;
+using Org.BouncyCastle.Pkcs;
+
+using Renci.SshNet.Common;
+using Renci.SshNet.Security;
+
+namespace Renci.SshNet
+{
+    public partial class PrivateKeyFile
+    {
+        private sealed class PKCS8 : IPrivateKeyParser
+        {
+            private readonly bool _encrypted;
+            private readonly byte[] _data;
+            private readonly string? _passPhrase;
+
+            public PKCS8(bool encrypted, byte[] data, string? passPhrase)
+            {
+                _encrypted = encrypted;
+                _data = data;
+                _passPhrase = passPhrase;
+            }
+
+            /// <summary>
+            /// Parses an OpenSSL PKCS#8 key file according to RFC5208:
+            /// <see href="https://www.rfc-editor.org/rfc/rfc5208#section-5"/>.
+            /// </summary>
+            /// <exception cref="SshException">Algorithm not supported.</exception>
+            public Key Parse()
+            {
+                PrivateKeyInfo privateKeyInfo;
+                if (_encrypted)
+                {
+                    var encryptedPrivateKeyInfo = EncryptedPrivateKeyInfo.GetInstance(_data);
+                    privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(_passPhrase?.ToCharArray(), encryptedPrivateKeyInfo);
+                }
+                else
+                {
+                    privateKeyInfo = PrivateKeyInfo.GetInstance(_data);
+                }
+
+                var algorithmOid = privateKeyInfo.PrivateKeyAlgorithm.Algorithm;
+                var key = privateKeyInfo.PrivateKey.GetOctets();
+                if (algorithmOid.Equals(PkcsObjectIdentifiers.RsaEncryption))
+                {
+                    return new RsaKey(key);
+                }
+
+                if (algorithmOid.Equals(X9ObjectIdentifiers.IdDsa))
+                {
+                    var parameters = privateKeyInfo.PrivateKeyAlgorithm.Parameters.GetDerEncoded();
+                    var parametersReader = new AsnReader(parameters, AsnEncodingRules.BER);
+                    var sequenceReader = parametersReader.ReadSequence();
+                    parametersReader.ThrowIfNotEmpty();
+
+                    var p = sequenceReader.ReadInteger();
+                    var q = sequenceReader.ReadInteger();
+                    var g = sequenceReader.ReadInteger();
+                    sequenceReader.ThrowIfNotEmpty();
+
+                    var keyReader = new AsnReader(key, AsnEncodingRules.BER);
+                    var x = keyReader.ReadInteger();
+                    keyReader.ThrowIfNotEmpty();
+
+                    var y = BigInteger.ModPow(g, x, p);
+
+                    return new DsaKey(p, q, g, y, x);
+                }
+
+                if (algorithmOid.Equals(X9ObjectIdentifiers.IdECPublicKey))
+                {
+                    var parameters = privateKeyInfo.PrivateKeyAlgorithm.Parameters.GetDerEncoded();
+                    var parametersReader = new AsnReader(parameters, AsnEncodingRules.DER);
+                    var curve = parametersReader.ReadObjectIdentifier();
+                    parametersReader.ThrowIfNotEmpty();
+
+                    var privateKeyReader = new AsnReader(key, AsnEncodingRules.DER);
+                    var sequenceReader = privateKeyReader.ReadSequence();
+                    privateKeyReader.ThrowIfNotEmpty();
+
+                    var version = sequenceReader.ReadInteger();
+                    if (version != BigInteger.One)
+                    {
+                        throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "EC version '{0}' is not supported.", version));
+                    }
+
+                    var privatekey = sequenceReader.ReadOctetString();
+
+                    var publicKeyReader = sequenceReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true));
+                    var publickey = publicKeyReader.ReadBitString(out _);
+                    publicKeyReader.ThrowIfNotEmpty();
+
+                    sequenceReader.ThrowIfNotEmpty();
+
+                    return new EcdsaKey(curve, publickey, privatekey.TrimLeadingZeros());
+                }
+
+                if (algorithmOid.Equals(EdECObjectIdentifiers.id_Ed25519))
+                {
+                    return new ED25519Key(key);
+                }
+
+                throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key algorithm \"{0}\" is not supported.", algorithmOid));
+            }
+        }
+    }
+}

+ 130 - 0
src/Renci.SshNet/PrivateKeyFile.SSHCOM.cs

@@ -0,0 +1,130 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography;
+using System.Text;
+
+using Renci.SshNet.Common;
+using Renci.SshNet.Security;
+using Renci.SshNet.Security.Cryptography.Ciphers;
+using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
+using Renci.SshNet.Security.Cryptography.Ciphers.Paddings;
+
+namespace Renci.SshNet
+{
+    public partial class PrivateKeyFile
+    {
+        private sealed class SSHCOM : IPrivateKeyParser
+        {
+            private readonly byte[] _data;
+            private readonly string? _passPhrase;
+
+            public SSHCOM(byte[] data, string? passPhrase)
+            {
+                _data = data;
+                _passPhrase = passPhrase;
+            }
+
+            public Key Parse()
+            {
+                var reader = new SshDataReader(_data);
+                var magicNumber = reader.ReadUInt32();
+                if (magicNumber != 0x3f6ff9eb)
+                {
+                    throw new SshException("Invalid SSH2 private key.");
+                }
+
+                _ = reader.ReadUInt32(); // Read total bytes length including magic number
+                var keyType = reader.ReadString(SshData.Ascii);
+                var ssh2CipherName = reader.ReadString(SshData.Ascii);
+                var blobSize = (int)reader.ReadUInt32();
+
+                byte[] keyData;
+                if (ssh2CipherName == "none")
+                {
+                    keyData = reader.ReadBytes(blobSize);
+                }
+                else if (ssh2CipherName == "3des-cbc")
+                {
+                    if (string.IsNullOrEmpty(_passPhrase))
+                    {
+                        throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
+                    }
+
+                    var key = GetCipherKey(_passPhrase, 192 / 8);
+                    var ssh2Сipher = new TripleDesCipher(key, new CbcCipherMode(new byte[8]), new PKCS7Padding());
+                    keyData = ssh2Сipher.Decrypt(reader.ReadBytes(blobSize));
+                }
+                else
+                {
+                    throw new SshException(string.Format("Cipher method '{0}' is not supported.", ssh2CipherName));
+                }
+
+                /*
+                 * TODO: Create two specific data types to avoid using SshDataReader class.
+                 */
+
+                reader = new SshDataReader(keyData);
+
+                var decryptedLength = reader.ReadUInt32();
+
+                if (decryptedLength > blobSize - 4)
+                {
+                    throw new SshException("Invalid passphrase.");
+                }
+
+                if (keyType.Contains("rsa"))
+                {
+                    var exponent = reader.ReadBigIntWithBits(); // e
+                    var d = reader.ReadBigIntWithBits(); // d
+                    var modulus = reader.ReadBigIntWithBits(); // n
+                    var inverseQ = reader.ReadBigIntWithBits(); // u
+                    var q = reader.ReadBigIntWithBits(); // p
+                    var p = reader.ReadBigIntWithBits(); // q
+                    return new RsaKey(modulus, exponent, d, p, q, inverseQ);
+                }
+                else if (keyType.Contains("dsa"))
+                {
+                    var zero = reader.ReadUInt32();
+                    if (zero != 0)
+                    {
+                        throw new SshException("Invalid private key");
+                    }
+
+                    var p = reader.ReadBigIntWithBits();
+                    var g = reader.ReadBigIntWithBits();
+                    var q = reader.ReadBigIntWithBits();
+                    var y = reader.ReadBigIntWithBits();
+                    var x = reader.ReadBigIntWithBits();
+                    return new DsaKey(p, q, g, y, x);
+                }
+
+                throw new NotSupportedException(string.Format("Key type '{0}' is not supported.", keyType));
+            }
+
+            private static byte[] GetCipherKey(string passphrase, int length)
+            {
+                var cipherKey = new List<byte>();
+
+#pragma warning disable CA1850 // Prefer static HashData method; We'll reuse the object on lower targets.
+                using (var md5 = MD5.Create())
+                {
+                    var passwordBytes = Encoding.UTF8.GetBytes(passphrase);
+
+                    var hash = md5.ComputeHash(passwordBytes);
+                    cipherKey.AddRange(hash);
+
+                    while (cipherKey.Count < length)
+                    {
+                        hash = passwordBytes.Concat(hash);
+                        hash = md5.ComputeHash(hash);
+                        cipherKey.AddRange(hash);
+                    }
+                }
+#pragma warning restore CA1850 // Prefer static HashData method
+
+                return cipherKey.ToArray().Take(length);
+            }
+        }
+    }
+}

+ 30 - 580
src/Renci.SshNet/PrivateKeyFile.cs

@@ -3,7 +3,6 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.Diagnostics.CodeAnalysis;
-using System.Formats.Asn1;
 using System.Globalization;
 using System.IO;
 using System.Linq;
@@ -12,17 +11,9 @@ using System.Security.Cryptography;
 using System.Text;
 using System.Text.RegularExpressions;
 
-using Org.BouncyCastle.Asn1.EdEC;
-using Org.BouncyCastle.Asn1.Pkcs;
-using Org.BouncyCastle.Asn1.X9;
-using Org.BouncyCastle.Pkcs;
-
 using Renci.SshNet.Common;
 using Renci.SshNet.Security;
 using Renci.SshNet.Security.Cryptography;
-using Renci.SshNet.Security.Cryptography.Ciphers;
-using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
-using Renci.SshNet.Security.Cryptography.Ciphers.Paddings;
 
 namespace Renci.SshNet
 {
@@ -305,606 +296,53 @@ namespace Renci.SshNet
             }
 
             var keyName = privateKeyMatch.Result("${keyName}");
-            if (!keyName.EndsWith("PRIVATE KEY", StringComparison.Ordinal))
-            {
-                throw new SshException("Invalid private key file.");
-            }
-
-            var cipherName = privateKeyMatch.Result("${cipherName}");
-            var salt = privateKeyMatch.Result("${salt}");
             var data = privateKeyMatch.Result("${data}");
-
             var binaryData = Convert.FromBase64String(data);
 
-            byte[] decryptedData;
-
-            if (!string.IsNullOrEmpty(cipherName) && !string.IsNullOrEmpty(salt))
-            {
-                if (string.IsNullOrEmpty(passPhrase))
-                {
-                    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);
-                }
-
-                CipherInfo cipher;
-                switch (cipherName)
-                {
-                    case "DES-EDE3-CBC":
-                        cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
-                        break;
-                    case "DES-EDE3-CFB":
-                        cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CfbCipherMode(iv), new PKCS7Padding()));
-                        break;
-                    case "DES-CBC":
-                        cipher = new CipherInfo(64, (key, iv) => new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
-                        break;
-                    case "AES-128-CBC":
-                        cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
-                        break;
-                    case "AES-192-CBC":
-                        cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
-                        break;
-                    case "AES-256-CBC":
-                        cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
-                        break;
-                    default:
-                        throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key cipher \"{0}\" is not supported.", cipherName));
-                }
-
-                decryptedData = DecryptKey(cipher, binaryData, passPhrase, binarySalt);
-            }
-            else
-            {
-                decryptedData = binaryData;
-            }
-
+            IPrivateKeyParser parser;
             switch (keyName)
             {
                 case "RSA PRIVATE KEY":
-                    var rsaKey = new RsaKey(decryptedData);
-                    _key = rsaKey;
-                    _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
-#pragma warning disable CA2000 // Dispose objects before losing scope
-                    _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512)));
-                    _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256)));
-#pragma warning restore CA2000 // Dispose objects before losing scope
-                    break;
                 case "DSA PRIVATE KEY":
-                    _key = new DsaKey(decryptedData);
-                    _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
-                    break;
                 case "EC PRIVATE KEY":
-                    _key = new EcdsaKey(decryptedData);
-                    _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
+                    var cipherName = privateKeyMatch.Result("${cipherName}");
+                    var salt = privateKeyMatch.Result("${salt}");
+                    parser = new PKCS1(cipherName, salt, keyName, binaryData, passPhrase);
                     break;
                 case "PRIVATE KEY":
-                    var privateKeyInfo = PrivateKeyInfo.GetInstance(binaryData);
-                    _key = ParseOpenSslPkcs8PrivateKey(privateKeyInfo);
-                    if (_key is RsaKey parsedRsaKey)
-                    {
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
-#pragma warning disable CA2000 // Dispose objects before losing scope
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey, HashAlgorithmName.SHA512)));
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey, HashAlgorithmName.SHA256)));
-#pragma warning restore CA2000 // Dispose objects before losing scope
-                    }
-                    else if (_key is DsaKey parsedDsaKey)
-                    {
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
-                    }
-                    else
-                    {
-                        _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
-                    }
-
+                    parser = new PKCS8(encrypted: false, binaryData, passPhrase);
                     break;
                 case "ENCRYPTED PRIVATE KEY":
-                    var encryptedPrivateKeyInfo = EncryptedPrivateKeyInfo.GetInstance(binaryData);
-                    privateKeyInfo = PrivateKeyInfoFactory.CreatePrivateKeyInfo(passPhrase?.ToCharArray(), encryptedPrivateKeyInfo);
-                    _key = ParseOpenSslPkcs8PrivateKey(privateKeyInfo);
-                    if (_key is RsaKey parsedRsaKey2)
-                    {
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
-#pragma warning disable CA2000 // Dispose objects before losing scope
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey2, HashAlgorithmName.SHA512)));
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey2, HashAlgorithmName.SHA256)));
-#pragma warning restore CA2000 // Dispose objects before losing scope
-                    }
-                    else if (_key is DsaKey parsedDsaKey)
-                    {
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
-                    }
-                    else
-                    {
-                        _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
-                    }
-
+                    parser = new PKCS8(encrypted: true, binaryData, passPhrase);
                     break;
                 case "OPENSSH PRIVATE KEY":
-                    _key = ParseOpenSshV1Key(decryptedData, passPhrase);
-                    if (_key is RsaKey parsedRsaKey3)
-                    {
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
-#pragma warning disable CA2000 // Dispose objects before losing scope
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey3, HashAlgorithmName.SHA512)));
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey3, HashAlgorithmName.SHA256)));
-#pragma warning restore CA2000 // Dispose objects before losing scope
-                    }
-                    else
-                    {
-                        _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
-                    }
-
+                    parser = new OpenSSH(binaryData, passPhrase);
                     break;
                 case "SSH2 ENCRYPTED PRIVATE KEY":
-                    var reader = new SshDataReader(decryptedData);
-                    var magicNumber = reader.ReadUInt32();
-                    if (magicNumber != 0x3f6ff9eb)
-                    {
-                        throw new SshException("Invalid SSH2 private key.");
-                    }
-
-                    _ = reader.ReadUInt32(); // Read total bytes length including magic number
-                    var keyType = reader.ReadString(SshData.Ascii);
-                    var ssh2CipherName = reader.ReadString(SshData.Ascii);
-                    var blobSize = (int)reader.ReadUInt32();
-
-                    byte[] keyData;
-                    if (ssh2CipherName == "none")
-                    {
-                        keyData = reader.ReadBytes(blobSize);
-                    }
-                    else if (ssh2CipherName == "3des-cbc")
-                    {
-                        if (string.IsNullOrEmpty(passPhrase))
-                        {
-                            throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
-                        }
-
-                        var key = GetCipherKey(passPhrase, 192 / 8);
-                        var ssh2Сipher = new TripleDesCipher(key, new CbcCipherMode(new byte[8]), new PKCS7Padding());
-                        keyData = ssh2Сipher.Decrypt(reader.ReadBytes(blobSize));
-                    }
-                    else
-                    {
-                        throw new SshException(string.Format("Cipher method '{0}' is not supported.", ssh2CipherName));
-                    }
-
-                    /*
-                     * TODO: Create two specific data types to avoid using SshDataReader class.
-                     */
-
-                    reader = new SshDataReader(keyData);
-
-                    var decryptedLength = reader.ReadUInt32();
-
-                    if (decryptedLength > blobSize - 4)
-                    {
-                        throw new SshException("Invalid passphrase.");
-                    }
-
-                    if (keyType.Contains("rsa"))
-                    {
-                        var exponent = reader.ReadBigIntWithBits(); // e
-                        var d = reader.ReadBigIntWithBits(); // d
-                        var modulus = reader.ReadBigIntWithBits(); // n
-                        var inverseQ = reader.ReadBigIntWithBits(); // u
-                        var q = reader.ReadBigIntWithBits(); // p
-                        var p = reader.ReadBigIntWithBits(); // q
-                        var decryptedRsaKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
-                        _key = decryptedRsaKey;
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
-#pragma warning disable CA2000 // Dispose objects before losing scope
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(decryptedRsaKey, HashAlgorithmName.SHA512)));
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(decryptedRsaKey, HashAlgorithmName.SHA256)));
-#pragma warning restore CA2000 // Dispose objects before losing scope
-                    }
-                    else if (keyType.Contains("dsa"))
-                    {
-                        var zero = reader.ReadUInt32();
-                        if (zero != 0)
-                        {
-                            throw new SshException("Invalid private key");
-                        }
-
-                        var p = reader.ReadBigIntWithBits();
-                        var g = reader.ReadBigIntWithBits();
-                        var q = reader.ReadBigIntWithBits();
-                        var y = reader.ReadBigIntWithBits();
-                        var x = reader.ReadBigIntWithBits();
-                        _key = new DsaKey(p, q, g, y, x);
-                        _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
-                    }
-                    else
-                    {
-                        throw new NotSupportedException(string.Format("Key type '{0}' is not supported.", keyType));
-                    }
-
+                    parser = new SSHCOM(binaryData, passPhrase);
                     break;
                 default:
                     throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", keyName));
             }
-        }
-
-        private static byte[] GetCipherKey(string passphrase, int length)
-        {
-            var cipherKey = new List<byte>();
-
-#pragma warning disable CA1850 // Prefer static HashData method; We'll reuse the object on lower targets.
-            using (var md5 = MD5.Create())
-            {
-                var passwordBytes = Encoding.UTF8.GetBytes(passphrase);
-
-                var hash = md5.ComputeHash(passwordBytes);
-                cipherKey.AddRange(hash);
-
-                while (cipherKey.Count < length)
-                {
-                    hash = passwordBytes.Concat(hash);
-                    hash = md5.ComputeHash(hash);
-                    cipherKey.AddRange(hash);
-                }
-            }
-#pragma warning restore CA1850 // Prefer static HashData method
-
-            return cipherKey.ToArray().Take(length);
-        }
-
-        /// <summary>
-        /// Decrypts encrypted private key file data.
-        /// </summary>
-        /// <param name="cipherInfo">The cipher info.</param>
-        /// <param name="cipherData">Encrypted data.</param>
-        /// <param name="passPhrase">Decryption pass phrase.</param>
-        /// <param name="binarySalt">Decryption binary salt.</param>
-        /// <returns>Decrypted byte array.</returns>
-        /// <exception cref="ArgumentNullException"><paramref name="cipherInfo" />, <paramref name="cipherData" />, <paramref name="passPhrase" /> or <paramref name="binarySalt" /> is <see langword="null"/>.</exception>
-        private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt)
-        {
-            Debug.Assert(cipherInfo != null);
-            Debug.Assert(cipherData != null);
-            Debug.Assert(binarySalt != null);
-
-            var cipherKey = new List<byte>();
-
-#pragma warning disable CA1850 // Prefer static HashData method; We'll reuse the object on lower targets.
-            using (var md5 = MD5.Create())
-            {
-                var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
-
-                // Use 8 bytes binary salt
-                var initVector = passwordBytes.Concat(binarySalt.Take(8));
-
-                var hash = md5.ComputeHash(initVector);
-                cipherKey.AddRange(hash);
-
-                while (cipherKey.Count < cipherInfo.KeySize / 8)
-                {
-                    hash = hash.Concat(initVector);
-                    hash = md5.ComputeHash(hash);
-                    cipherKey.AddRange(hash);
-                }
-            }
-#pragma warning restore CA1850 // Prefer static HashData method
-
-            var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
-
-            try
-            {
-                return cipher.Decrypt(cipherData);
-            }
-            finally
-            {
-                if (cipher is IDisposable disposable)
-                {
-                    disposable.Dispose();
-                }
-            }
-        }
-
-        /// <summary>
-        /// Parses an OpenSSH V1 key file according to the key spec:
-        /// <see href="https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key"/>.
-        /// </summary>
-        /// <param name="keyFileData">The key file data (i.e. base64 encoded data between the header/footer).</param>
-        /// <param name="passPhrase">Passphrase or <see langword="null"/> if there isn't one.</param>
-        /// <returns>
-        /// The OpenSSH V1 key.
-        /// </returns>
-        private static Key ParseOpenSshV1Key(byte[] keyFileData, string? passPhrase)
-        {
-            var keyReader = new SshDataReader(keyFileData);
-
-            // check magic header
-            var authMagic = "openssh-key-v1\0"u8;
-            var keyHeaderBytes = keyReader.ReadBytes(authMagic.Length);
-            if (!authMagic.SequenceEqual(keyHeaderBytes))
-            {
-                throw new SshException("This openssh key does not contain the 'openssh-key-v1' format magic header");
-            }
-
-            // cipher will be "aes256-cbc" or other cipher if using a passphrase, "none" otherwise
-            var cipherName = keyReader.ReadString(Encoding.UTF8);
 
-            // key derivation function (kdf): bcrypt or nothing
-            var kdfName = keyReader.ReadString(Encoding.UTF8);
+            _key = parser.Parse();
 
-            // kdf options length: 24 if passphrase, 0 if no passphrase
-            var kdfOptionsLen = (int)keyReader.ReadUInt32();
-            byte[]? salt = null;
-            var rounds = 0;
-            if (kdfOptionsLen > 0)
+            if (_key is RsaKey rsaKey)
             {
-                var saltLength = (int)keyReader.ReadUInt32();
-                salt = keyReader.ReadBytes(saltLength);
-                rounds = (int)keyReader.ReadUInt32();
+                _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
+#pragma warning disable CA2000 // Dispose objects before losing scope
+                _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512)));
+                _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256)));
+#pragma warning restore CA2000 // Dispose objects before losing scope
             }
-
-            // number of public keys, only supporting 1 for now
-            var numberOfPublicKeys = (int)keyReader.ReadUInt32();
-            if (numberOfPublicKeys != 1)
+            else if (_key is DsaKey)
             {
-                throw new SshException("At this time only one public key in the openssh key is supported.");
-            }
-
-            // read public key in ssh-format, but we dont need it
-            _ = keyReader.ReadString(Encoding.UTF8);
-
-            // possibly encrypted private key
-            var privateKeyLength = (int)keyReader.ReadUInt32();
-            byte[] privateKeyBytes;
-
-            // decrypt private key if necessary
-            if (cipherName != "none")
-            {
-                if (string.IsNullOrEmpty(passPhrase))
-                {
-                    throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
-                }
-
-                if (string.IsNullOrEmpty(kdfName) || kdfName != "bcrypt")
-                {
-                    throw new SshException("kdf " + kdfName + " is not supported for openssh key file");
-                }
-
-                var ivLength = 16;
-                CipherInfo cipherInfo;
-                switch (cipherName)
-                {
-                    case "3des-cbc":
-                        ivLength = 8;
-                        cipherInfo = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), padding: null));
-                        break;
-                    case "aes128-cbc":
-                        cipherInfo = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
-                        break;
-                    case "aes192-cbc":
-                        cipherInfo = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
-                        break;
-                    case "aes256-cbc":
-                        cipherInfo = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false));
-                        break;
-                    case "aes128-ctr":
-                        cipherInfo = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
-                        break;
-                    case "aes192-ctr":
-                        cipherInfo = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
-                        break;
-                    case "aes256-ctr":
-                        cipherInfo = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false));
-                        break;
-                    case "aes128-gcm@openssh.com":
-                        cipherInfo = new CipherInfo(128, (key, iv) => new AesGcmCipher(key, iv, aadLength: 0), isAead: true);
-                        break;
-                    case "aes256-gcm@openssh.com":
-                        cipherInfo = new CipherInfo(256, (key, iv) => new AesGcmCipher(key, iv, aadLength: 0), isAead: true);
-                        break;
-                    case "chacha20-poly1305@openssh.com":
-                        ivLength = 12;
-                        cipherInfo = new CipherInfo(256, (key, iv) => new ChaCha20Poly1305Cipher(key, aadLength: 0), isAead: true);
-                        break;
-                    default:
-                        throw new SshException("Cipher '" + cipherName + "' is not supported for an OpenSSH key.");
-                }
-
-                var keyLength = cipherInfo.KeySize / 8;
-
-                // inspired by the SSHj library (https://github.com/hierynomus/sshj)
-                // apply the kdf to derive a key and iv from the passphrase
-                var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
-                var keyiv = new byte[keyLength + ivLength];
-                new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv);
-
-                var key = keyiv.Take(keyLength);
-                var iv = keyiv.Take(keyLength, ivLength);
-
-                var cipher = cipherInfo.Cipher(key, iv);
-
-                // The authentication tag data (if any) is concatenated to the end of the encrypted private key string.
-                // See https://github.com/openssh/openssh-portable/blob/509b757c052ea969b3a41fc36818b44801caf1cf/sshkey.c#L2951
-                // and https://github.com/openssh/openssh-portable/blob/509b757c052ea969b3a41fc36818b44801caf1cf/cipher.c#L340
-                var cipherData = keyReader.ReadBytes(privateKeyLength + cipher.TagSize);
-
-                try
-                {
-                    privateKeyBytes = cipher.Decrypt(cipherData, 0, privateKeyLength);
-                }
-                finally
-                {
-                    if (cipher is IDisposable disposable)
-                    {
-                        disposable.Dispose();
-                    }
-                }
+                _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
             }
             else
             {
-                privateKeyBytes = keyReader.ReadBytes(privateKeyLength);
-            }
-
-            // validate private key length
-            privateKeyLength = privateKeyBytes.Length;
-            if (privateKeyLength % 8 != 0)
-            {
-                throw new SshException("The private key section must be a multiple of the block size (8)");
-            }
-
-            // now parse the data we called the private key, it actually contains the public key again
-            // so we need to parse through it to get the private key bytes, plus there's some
-            // validation we need to do.
-            var privateKeyReader = new SshDataReader(privateKeyBytes);
-
-            // check ints should match, they wouldn't match for example if the wrong passphrase was supplied
-            var checkInt1 = (int)privateKeyReader.ReadUInt32();
-            var checkInt2 = (int)privateKeyReader.ReadUInt32();
-            if (checkInt1 != checkInt2)
-            {
-                throw new SshException(string.Format(CultureInfo.InvariantCulture,
-                                                     "The random check bytes of the OpenSSH key do not match ({0} <-> {1}).",
-                                                     checkInt1.ToString(CultureInfo.InvariantCulture),
-                                                     checkInt2.ToString(CultureInfo.InvariantCulture)));
-            }
-
-            // key type
-            var keyType = privateKeyReader.ReadString(Encoding.UTF8);
-
-            Key parsedKey;
-            byte[] publicKey;
-            byte[] unencryptedPrivateKey;
-            switch (keyType)
-            {
-                case "ssh-ed25519":
-                    // https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-11#section-3.2.3
-
-                    // ENC(A)
-                    _ = privateKeyReader.ReadBignum2();
-
-                    // k || ENC(A)
-                    unencryptedPrivateKey = privateKeyReader.ReadBignum2();
-                    parsedKey = new ED25519Key(unencryptedPrivateKey);
-                    break;
-                case "ecdsa-sha2-nistp256":
-                case "ecdsa-sha2-nistp384":
-                case "ecdsa-sha2-nistp521":
-                    // curve
-                    var len = (int)privateKeyReader.ReadUInt32();
-                    var curve = Encoding.ASCII.GetString(privateKeyReader.ReadBytes(len));
-
-                    // public key
-                    publicKey = privateKeyReader.ReadBignum2();
-
-                    // private key
-                    unencryptedPrivateKey = privateKeyReader.ReadBignum2();
-                    parsedKey = new EcdsaKey(curve, publicKey, unencryptedPrivateKey.TrimLeadingZeros());
-                    break;
-                case "ssh-rsa":
-                    var modulus = privateKeyReader.ReadBignum(); // n
-                    var exponent = privateKeyReader.ReadBignum(); // e
-                    var d = privateKeyReader.ReadBignum(); // d
-                    var inverseQ = privateKeyReader.ReadBignum(); // iqmp
-                    var p = privateKeyReader.ReadBignum(); // p
-                    var q = privateKeyReader.ReadBignum(); // q
-                    parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
-                    break;
-                default:
-                    throw new SshException("OpenSSH key type '" + keyType + "' is not supported.");
-            }
-
-            parsedKey.Comment = privateKeyReader.ReadString(Encoding.UTF8);
-
-            // The list of privatekey/comment pairs is padded with the bytes 1, 2, 3, ...
-            // until the total length is a multiple of the cipher block size.
-            var padding = privateKeyReader.ReadBytes();
-            for (var i = 0; i < padding.Length; i++)
-            {
-                if ((int)padding[i] != i + 1)
-                {
-                    throw new SshException("Padding of openssh key format contained wrong byte at position: " +
-                                           i.ToString(CultureInfo.InvariantCulture));
-                }
-            }
-
-            return parsedKey;
-        }
-
-        /// <summary>
-        /// Parses an OpenSSL PKCS#8 key file according to RFC5208:
-        /// <see href="https://www.rfc-editor.org/rfc/rfc5208#section-5"/>.
-        /// </summary>
-        /// <param name="privateKeyInfo">The <see cref="PrivateKeyInfo"/>.</param>
-        /// <returns>
-        /// The <see cref="Key"/>.
-        /// </returns>
-        /// <exception cref="SshException">Algorithm not supported.</exception>
-        private static Key ParseOpenSslPkcs8PrivateKey(PrivateKeyInfo privateKeyInfo)
-        {
-            var algorithmOid = privateKeyInfo.PrivateKeyAlgorithm.Algorithm;
-            var key = privateKeyInfo.PrivateKey.GetOctets();
-            if (algorithmOid.Equals(PkcsObjectIdentifiers.RsaEncryption))
-            {
-                return new RsaKey(key);
-            }
-
-            if (algorithmOid.Equals(X9ObjectIdentifiers.IdDsa))
-            {
-                var parameters = privateKeyInfo.PrivateKeyAlgorithm.Parameters.GetDerEncoded();
-                var parametersReader = new AsnReader(parameters, AsnEncodingRules.BER);
-                var sequenceReader = parametersReader.ReadSequence();
-                parametersReader.ThrowIfNotEmpty();
-
-                var p = sequenceReader.ReadInteger();
-                var q = sequenceReader.ReadInteger();
-                var g = sequenceReader.ReadInteger();
-                sequenceReader.ThrowIfNotEmpty();
-
-                var keyReader = new AsnReader(key, AsnEncodingRules.BER);
-                var x = keyReader.ReadInteger();
-                keyReader.ThrowIfNotEmpty();
-
-                var y = BigInteger.ModPow(g, x, p);
-
-                return new DsaKey(p, q, g, y, x);
-            }
-
-            if (algorithmOid.Equals(X9ObjectIdentifiers.IdECPublicKey))
-            {
-                var parameters = privateKeyInfo.PrivateKeyAlgorithm.Parameters.GetDerEncoded();
-                var parametersReader = new AsnReader(parameters, AsnEncodingRules.DER);
-                var curve = parametersReader.ReadObjectIdentifier();
-                parametersReader.ThrowIfNotEmpty();
-
-                var privateKeyReader = new AsnReader(key, AsnEncodingRules.DER);
-                var sequenceReader = privateKeyReader.ReadSequence();
-                privateKeyReader.ThrowIfNotEmpty();
-
-                var version = sequenceReader.ReadInteger();
-                if (version != BigInteger.One)
-                {
-                    throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "EC version '{0}' is not supported.", version));
-                }
-
-                var privatekey = sequenceReader.ReadOctetString();
-
-                var publicKeyReader = sequenceReader.ReadSequence(new Asn1Tag(TagClass.ContextSpecific, 1, isConstructed: true));
-                var publickey = publicKeyReader.ReadBitString(out _);
-                publicKeyReader.ThrowIfNotEmpty();
-
-                sequenceReader.ThrowIfNotEmpty();
-
-                return new EcdsaKey(curve, publickey, privatekey.TrimLeadingZeros());
-            }
-
-            if (algorithmOid.Equals(EdECObjectIdentifiers.id_Ed25519))
-            {
-                return new ED25519Key(key);
+                _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
             }
-
-            throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key algorithm \"{0}\" is not supported.", algorithmOid));
         }
 
         /// <summary>
@@ -1052,5 +490,17 @@ namespace Renci.SshNet
             {
             }
         }
+
+        /// <summary>
+        /// Represents private key parser.
+        /// </summary>
+        private interface IPrivateKeyParser
+        {
+            /// <summary>
+            /// Parses the private key.
+            /// </summary>
+            /// <returns>The <see cref="Key"/>.</returns>
+            Key Parse();
+        }
     }
 }