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()
{
}
}
}
}