#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 CipherMode = System.Security.Cryptography.CipherMode; 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."); } #if NET var binarySalt = Convert.FromHexString(_salt); #else var binarySalt = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_salt); #endif CipherInfo cipher; switch (_cipherName) { case "DES-EDE3-CBC": cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, iv, CipherMode.CBC, pkcs7Padding: true)); break; case "DES-EDE3-CFB": cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, iv, CipherMode.CFB, pkcs7Padding: false)); 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(); } } } } } }