using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Text; using System.Text.RegularExpressions; using Renci.SshNet.Abstractions; 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 { /// /// Represents private key information. /// /// /// /// /// /// /// The following private keys are supported: /// /// /// RSA in OpenSSL PEM, ssh.com and OpenSSH key format /// /// /// DSA in OpenSSL PEM and ssh.com format /// /// /// ECDSA 256/384/521 in OpenSSL PEM and OpenSSH key format /// /// /// ED25519 in OpenSSH key 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 : IPrivateKeySource, 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 *-+", RegexOptions.Compiled | RegexOptions.Multiline); private Key _key; private bool _isDisposed; /// /// Gets the host key. /// public HostAlgorithm HostKey { get; private set; } /// /// Initializes a new instance of the class. /// /// The key. public PrivateKeyFile(Key key) { HostKey = new KeyHostAlgorithm(key.ToString(), key); } /// /// Initializes a new instance of the class. /// /// The private key. public PrivateKeyFile(Stream privateKey) { Open(privateKey, passPhrase: 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, passPhrase: 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(nameof(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. private void Open(Stream privateKey, string passPhrase) { if (privateKey is null) { throw new ArgumentNullException(nameof(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 "EC": _key = new EcdsaKey(decryptedData); HostKey = new KeyHostAlgorithm(_key.ToString(), _key); break; case "OPENSSH": _key = ParseOpenSshV1Key(decryptedData, passPhrase); HostKey = new KeyHostAlgorithm(_key.ToString(), _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 is null) { throw new ArgumentNullException(nameof(cipherInfo)); } if (cipherData is null) { throw new ArgumentNullException(nameof(cipherData)); } if (binarySalt is null) { throw new ArgumentNullException(nameof(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); } /// /// Parses an OpenSSH V1 key file (i.e. ED25519 key) according to the the key spec: /// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key. /// /// the key file data (i.e. base64 encoded data between the header/footer) /// passphrase or null 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 = Encoding.UTF8.GetBytes("openssh-key-v1\0"); var keyHeaderBytes = keyReader.ReadBytes(authMagic.Length); if (!authMagic.IsEqualTo(keyHeaderBytes)) { throw new SshException("This openssh key does not contain the 'openssh-key-v1' format magic header"); } // cipher will be "aes256-cbc" 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(); var privateKeyBytes = keyReader.ReadBytes(privateKeyLength); // 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"); } // 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[48]; new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv); var key = new byte[32]; Array.Copy(keyiv, 0, key, 0, 32); var iv = new byte[16]; Array.Copy(keyiv, 32, iv, 0, 16); AesCipher cipher; switch (cipherName) { case "aes256-cbc": cipher = new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); break; case "aes256-ctr": cipher = new AesCipher(key, new CtrCipherMode(iv), new PKCS7Padding()); break; default: throw new SshException("Cipher '" + cipherName + "' is not supported for an OpenSSH key."); } privateKeyBytes = cipher.Decrypt(privateKeyBytes); } // 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("The random check bytes of the OpenSSH key do not match (" + checkInt1 + " <->" + checkInt2 + ")."); } // key type var keyType = privateKeyReader.ReadString(Encoding.UTF8); Key parsedKey; byte[] publicKey; byte[] unencryptedPrivateKey; switch (keyType) { case "ssh-ed25519": // public key publicKey = privateKeyReader.ReadBignum2(); // private key unencryptedPrivateKey = privateKeyReader.ReadBignum2(); parsedKey = new ED25519Key(publicKey.Reverse(), 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); } } return parsedKey; } /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// public void Dispose() { Dispose(disposing: 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; } } /// /// Finalizes an instance of the class. /// ~PrivateKeyFile() { Dispose(disposing: false); } private sealed 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); } public new byte[] ReadBytes() { return base.ReadBytes(); } /// /// 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()); } public BigInteger ReadBignum() { return new BigInteger(ReadBignum2().Reverse()); } public byte[] ReadBignum2() { var length = (int)base.ReadUInt32(); return base.ReadBytes(length); } protected override void LoadData() { } protected override void SaveData() { } } } }