using System; using System.Collections.Generic; using System.IO; using System.Text; using System.Text.RegularExpressions; using Renci.SshNet.Abstractions; using Renci.SshNet.Security; using Renci.SshNet.Common; using System.Globalization; using Renci.SshNet.Security.Cryptography.Ciphers; using Renci.SshNet.Security.Cryptography.Ciphers.Modes; using Renci.SshNet.Security.Cryptography.Ciphers.Paddings; using System.Diagnostics.CodeAnalysis; namespace Renci.SshNet { /// /// Represents private key information. /// /// /// /// /// /// /// Supports RSA and DSA private key in both OpenSSH and ssh.com format. /// /// /// The following encryption algorithms are supported: /// /// /// DES-EDE3-CBC /// /// /// DES-EDE3-CFB /// /// /// DES-CBC /// /// /// AES-128-CBC /// /// /// AES-192-CBC /// /// /// AES-256-CBC /// /// /// /// public class PrivateKeyFile : IDisposable { private static readonly Regex PrivateKeyRegex = new Regex(@"^-+ *BEGIN (?\w+( \w+)*) PRIVATE KEY *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?[A-Z0-9-]+),(?[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?([a-zA-Z0-9/+=]{1,80}\r?\n)+)-+ *END \k PRIVATE KEY *-+", #if FEATURE_REGEX_COMPILE RegexOptions.Compiled | RegexOptions.Multiline); #else RegexOptions.Multiline); #endif private Key _key; /// /// Gets the host key. /// public HostAlgorithm HostKey { get; private set; } /// /// Initializes a new instance of the class. /// /// The private key. public PrivateKeyFile(Stream privateKey) { Open(privateKey, null); } /// /// Initializes a new instance of the class. /// /// Name of the file. /// is null or empty. /// This method calls internally, this method does not catch exceptions from . public PrivateKeyFile(string fileName) : this(fileName, null) { } /// /// Initializes a new instance of the class. /// /// Name of the file. /// The pass phrase. /// is null or empty, or is null. /// This method calls internally, this method does not catch exceptions from . public PrivateKeyFile(string fileName, string passPhrase) { if (string.IsNullOrEmpty(fileName)) throw new ArgumentNullException("fileName"); using (var keyFile = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) { Open(keyFile, passPhrase); } } /// /// Initializes a new instance of the class. /// /// The private key. /// The pass phrase. /// or is null. public PrivateKeyFile(Stream privateKey, string passPhrase) { Open(privateKey, passPhrase); } /// /// Opens the specified private key. /// /// The private key. /// The pass phrase. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "this._key disposed in Dispose(bool) method.")] private void Open(Stream privateKey, string passPhrase) { if (privateKey == null) throw new ArgumentNullException("privateKey"); Match privateKeyMatch; using (var sr = new StreamReader(privateKey)) { var text = sr.ReadToEnd(); privateKeyMatch = PrivateKeyRegex.Match(text); } if (!privateKeyMatch.Success) { throw new SshException("Invalid private key file."); } var keyName = privateKeyMatch.Result("${keyName}"); 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, new CbcCipherMode(iv), new PKCS7Padding())); break; case "AES-192-CBC": cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding())); break; case "AES-256-CBC": cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding())); break; default: throw new SshException(string.Format(CultureInfo.CurrentCulture, "Private key cipher \"{0}\" is not supported.", cipherName)); } decryptedData = DecryptKey(cipher, binaryData, passPhrase, binarySalt); } else { decryptedData = binaryData; } switch (keyName) { case "RSA": _key = new RsaKey(decryptedData); HostKey = new KeyHostAlgorithm("ssh-rsa", _key); break; case "DSA": _key = new DsaKey(decryptedData); HostKey = new KeyHostAlgorithm("ssh-dss", _key); break; case "SSH2 ENCRYPTED": 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 == "if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}") { 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 _key = new RsaKey(modulus, exponent, d, p, q, inverseQ); HostKey = new KeyHostAlgorithm("ssh-rsa", _key); } else if (keyType == "dl-modp{sign{dsa-nist-sha1},dh{plain}}") { 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); HostKey = new KeyHostAlgorithm("ssh-dss", _key); } else { throw new NotSupportedException(string.Format("Key type '{0}' is not supported.", keyType)); } 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(); using (var md5 = CryptoAbstraction.CreateMD5()) { 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); } } 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 null. private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt) { if (cipherInfo == null) throw new ArgumentNullException("cipherInfo"); if (cipherData == null) throw new ArgumentNullException("cipherData"); if (binarySalt == null) throw new ArgumentNullException("binarySalt"); var cipherKey = new List(); using (var md5 = CryptoAbstraction.CreateMD5()) { 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); } } var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt); return cipher.Decrypt(cipherData); } #region IDisposable Members private bool _isDisposed; /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// Releases unmanaged and - optionally - managed resources /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected virtual void Dispose(bool disposing) { if (_isDisposed) return; if (disposing) { var key = _key; if (key != null) { ((IDisposable) key).Dispose(); _key = null; } _isDisposed = true; } } /// /// Releases unmanaged resources and performs other cleanup operations before the /// is reclaimed by garbage collection. /// ~PrivateKeyFile() { Dispose(false); } #endregion private class SshDataReader : SshData { public SshDataReader(byte[] data) { Load(data); } public new uint ReadUInt32() { return base.ReadUInt32(); } public new string ReadString(Encoding encoding) { return base.ReadString(encoding); } public new byte[] ReadBytes(int length) { return base.ReadBytes(length); } /// /// Reads next mpint data type from internal buffer where length specified in bits. /// /// mpint read. public BigInteger ReadBigIntWithBits() { var length = (int) base.ReadUInt32(); length = (length + 7) / 8; var data = base.ReadBytes(length); var bytesArray = new byte[data.Length + 1]; Buffer.BlockCopy(data, 0, bytesArray, 1, data.Length); return new BigInteger(bytesArray.Reverse()); } protected override void LoadData() { } protected override void SaveData() { } } } }