From 7dc05817f45f1cba899d52fd3d605f0f665c5535 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Sun, 17 Nov 2024 14:47:00 +0800 Subject: [PATCH 1/2] Split PrivateKeyFile into different implementations. --- src/Renci.SshNet/PrivateKeyFile.OpenSSH.cs | 249 +++++++++ src/Renci.SshNet/PrivateKeyFile.PKCS1.cs | 150 ++++++ src/Renci.SshNet/PrivateKeyFile.PKCS8.cs | 115 ++++ src/Renci.SshNet/PrivateKeyFile.SSHCOM.cs | 130 +++++ src/Renci.SshNet/PrivateKeyFile.cs | 599 +-------------------- 5 files changed, 672 insertions(+), 571 deletions(-) create mode 100644 src/Renci.SshNet/PrivateKeyFile.OpenSSH.cs create mode 100644 src/Renci.SshNet/PrivateKeyFile.PKCS1.cs create mode 100644 src/Renci.SshNet/PrivateKeyFile.PKCS8.cs create mode 100644 src/Renci.SshNet/PrivateKeyFile.SSHCOM.cs diff --git a/src/Renci.SshNet/PrivateKeyFile.OpenSSH.cs b/src/Renci.SshNet/PrivateKeyFile.OpenSSH.cs new file mode 100644 index 000000000..ead73bb50 --- /dev/null +++ b/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; + } + + /// + /// Parses an OpenSSH V1 key file according to the key spec: + /// . + /// + 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; + } + } + } +} diff --git a/src/Renci.SshNet/PrivateKeyFile.PKCS1.cs b/src/Renci.SshNet/PrivateKeyFile.PKCS1.cs new file mode 100644 index 000000000..0ef8d4bc2 --- /dev/null +++ b/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)); + } + } + + /// + /// Decrypts encrypted private key file data. + /// + /// The cipher info. + /// Encrypted data. + /// Decryption pass phrase. + /// Decryption binary salt. + /// Decrypted byte array. + /// , , or is . + 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(); + +#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(); + } + } + } + } + } +} diff --git a/src/Renci.SshNet/PrivateKeyFile.PKCS8.cs b/src/Renci.SshNet/PrivateKeyFile.PKCS8.cs new file mode 100644 index 000000000..ad4586155 --- /dev/null +++ b/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; + } + + /// + /// Parses an OpenSSL PKCS#8 key file according to RFC5208: + /// . + /// + /// Algorithm not supported. + 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)); + } + } + } +} diff --git a/src/Renci.SshNet/PrivateKeyFile.SSHCOM.cs b/src/Renci.SshNet/PrivateKeyFile.SSHCOM.cs new file mode 100644 index 000000000..9fbbfe44b --- /dev/null +++ b/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(); + +#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); + } + } + } +} diff --git a/src/Renci.SshNet/PrivateKeyFile.cs b/src/Renci.SshNet/PrivateKeyFile.cs index 414bcfcd7..55530a470 100644 --- a/src/Renci.SshNet/PrivateKeyFile.cs +++ b/src/Renci.SshNet/PrivateKeyFile.cs @@ -3,7 +3,6 @@ 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.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 { @@ -316,595 +307,49 @@ private void Open(Stream privateKey, string? passPhrase) 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)); + 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.", cipherName)); - } - - /* - * 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(); - -#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); - } - - /// - /// Decrypts encrypted private key file data. - /// - /// The cipher info. - /// Encrypted data. - /// Decryption pass phrase. - /// Decryption binary salt. - /// Decrypted byte array. - /// , , or is . - 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(); - -#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(); - } - } - } - - /// - /// Parses an OpenSSH V1 key file according to the key spec: - /// . - /// - /// The key file data (i.e. base64 encoded data between the header/footer). - /// Passphrase or if there isn't one. - /// - /// The OpenSSH V1 key. - /// - 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); - // 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(); - } + _key = parser.Parse(); - // number of public keys, only supporting 1 for now - var numberOfPublicKeys = (int)keyReader.ReadUInt32(); - if (numberOfPublicKeys != 1) + if (_key is RsaKey rsaKey) { - throw new SshException("At this time only one public key in the openssh key is supported."); + _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 } - - // 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") + else if (_key is DsaKey) { - 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; - } - - /// - /// Parses an OpenSSL PKCS#8 key file according to RFC5208: - /// . - /// - /// The . - /// - /// The . - /// - /// Algorithm not supported. - 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)); } /// @@ -1052,5 +497,17 @@ protected override void SaveData() { } } + + /// + /// Represents private key parser. + /// + private interface IPrivateKeyParser + { + /// + /// Parses the private key. + /// + /// The . + Key Parse(); + } } } From b13a8306e6a3c30f9dd813dec182f27d85a12a43 Mon Sep 17 00:00:00 2001 From: Scott Xu Date: Mon, 18 Nov 2024 22:03:33 +0800 Subject: [PATCH 2/2] Remove duplicate keyName check. Get cipherName and salt only if the key is PKCS1 format. --- src/Renci.SshNet/PrivateKeyFile.cs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/Renci.SshNet/PrivateKeyFile.cs b/src/Renci.SshNet/PrivateKeyFile.cs index 55530a470..4a575cd29 100644 --- a/src/Renci.SshNet/PrivateKeyFile.cs +++ b/src/Renci.SshNet/PrivateKeyFile.cs @@ -296,24 +296,17 @@ private void Open(Stream privateKey, string? passPhrase) } 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); IPrivateKeyParser parser; - switch (keyName) { case "RSA PRIVATE KEY": case "DSA PRIVATE KEY": case "EC PRIVATE KEY": + var cipherName = privateKeyMatch.Result("${cipherName}"); + var salt = privateKeyMatch.Result("${salt}"); parser = new PKCS1(cipherName, salt, keyName, binaryData, passPhrase); break; case "PRIVATE KEY":