You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

439 lines
17 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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
{
/// <summary>
/// Represents private key information.
/// </summary>
/// <example>
/// <code source="..\..\src\Renci.SshNet.Tests\Data\Key.RSA.txt" language="Text" title="Private RSA key example" />
/// </example>
/// <remarks>
/// <para>
/// Supports RSA and DSA private key in both <c>OpenSSH</c> and <c>ssh.com</c> format.
/// </para>
/// <para>
/// The following encryption algorithms are supported:
/// <list type="bullet">
/// <item>
/// <description>DES-EDE3-CBC</description>
/// </item>
/// <item>
/// <description>DES-EDE3-CFB</description>
/// </item>
/// <item>
/// <description>DES-CBC</description>
/// </item>
/// <item>
/// <description>AES-128-CBC</description>
/// </item>
/// <item>
/// <description>AES-192-CBC</description>
/// </item>
/// <item>
/// <description>AES-256-CBC</description>
/// </item>
/// </list>
/// </para>
/// </remarks>
public class PrivateKeyFile : IDisposable
{
private static readonly Regex PrivateKeyRegex = new Regex(@"^-+ *BEGIN (?<keyName>\w+( \w+)*) PRIVATE KEY *-+\r?\n((Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]+)\r?\n\r?\n)|(Comment: ""?[^\r\n]*""?\r?\n))?(?<data>([a-zA-Z0-9/+=]{1,80}\r?\n)+)-+ *END \k<keyName> PRIVATE KEY *-+",
#if FEATURE_REGEX_COMPILE
RegexOptions.Compiled | RegexOptions.Multiline);
#else
RegexOptions.Multiline);
#endif
private Key _key;
/// <summary>
/// Gets the host key.
/// </summary>
public HostAlgorithm HostKey { get; private set; }
/// <summary>
/// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
/// </summary>
/// <param name="privateKey">The private key.</param>
public PrivateKeyFile(Stream privateKey)
{
Open(privateKey, null);
}
/// <summary>
/// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
/// </summary>
/// <param name="fileName">Name of the file.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <c>null</c> or empty.</exception>
/// <remarks>This method calls <see cref="System.IO.File.Open(string, System.IO.FileMode)"/> internally, this method does not catch exceptions from <see cref="System.IO.File.Open(string, System.IO.FileMode)"/>.</remarks>
public PrivateKeyFile(string fileName)
: this(fileName, null)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
/// </summary>
/// <param name="fileName">Name of the file.</param>
/// <param name="passPhrase">The pass phrase.</param>
/// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <c>null</c> or empty, or <paramref name="passPhrase"/> is <c>null</c>.</exception>
/// <remarks>This method calls <see cref="System.IO.File.Open(string, System.IO.FileMode)"/> internally, this method does not catch exceptions from <see cref="System.IO.File.Open(string, System.IO.FileMode)"/>.</remarks>
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);
}
}
/// <summary>
/// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
/// </summary>
/// <param name="privateKey">The private key.</param>
/// <param name="passPhrase">The pass phrase.</param>
/// <exception cref="ArgumentNullException"><paramref name="privateKey"/> or <paramref name="passPhrase"/> is <c>null</c>.</exception>
public PrivateKeyFile(Stream privateKey, string passPhrase)
{
Open(privateKey, passPhrase);
}
/// <summary>
/// Opens the specified private key.
/// </summary>
/// <param name="privateKey">The private key.</param>
/// <param name="passPhrase">The pass phrase.</param>
[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<byte>();
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);
}
/// <summary>
/// Decrypts encrypted private key file data.
/// </summary>
/// <param name="cipherInfo">The cipher info.</param>
/// <param name="cipherData">Encrypted data.</param>
/// <param name="passPhrase">Decryption pass phrase.</param>
/// <param name="binarySalt">Decryption binary salt.</param>
/// <returns>Decrypted byte array.</returns>
/// <exception cref="ArgumentNullException"><paramref name="cipherInfo" />, <paramref name="cipherData" />, <paramref name="passPhrase" /> or <paramref name="binarySalt" /> is <c>null</c>.</exception>
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<byte>();
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;
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (_isDisposed)
return;
if (disposing)
{
var key = _key;
if (key != null)
{
((IDisposable) key).Dispose();
_key = null;
}
_isDisposed = true;
}
}
/// <summary>
/// Releases unmanaged resources and performs other cleanup operations before the
/// <see cref="PrivateKeyFile"/> is reclaimed by garbage collection.
/// </summary>
~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);
}
/// <summary>
/// Reads next mpint data type from internal buffer where length specified in bits.
/// </summary>
/// <returns>mpint read.</returns>
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()
{
}
}
}
}