diff --git a/src/Renci.SshNet/PrivateKeyFile.cs b/src/Renci.SshNet/PrivateKeyFile.cs
index f44672347..0437204d3 100644
--- a/src/Renci.SshNet/PrivateKeyFile.cs
+++ b/src/Renci.SshNet/PrivateKeyFile.cs
@@ -11,6 +11,8 @@
using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
using Renci.SshNet.Security.Cryptography.Ciphers.Paddings;
using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using Renci.SshNet.Security.Cryptography;
namespace Renci.SshNet
{
@@ -22,7 +24,7 @@ namespace Renci.SshNet
///
///
///
- /// Supports RSA and DSA private key in both OpenSSH and ssh.com format.
+ /// Supports RSA and DSA private key in OpenSSH, ssh.com, and PuTTY formats.
///
///
/// The following encryption algorithms are supported:
@@ -50,7 +52,21 @@ namespace Renci.SshNet
///
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 *-+",
+ private static readonly Regex SshPrivateKeyRegex = 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 static readonly Regex PuttyPrivateKeyRegex = new Regex(
+ @"^PuTTY-User-Key-File-(?[0-9]+): *(?[^\r\n]+)(\r|\n)+" +
+ @"Encryption: *(?[^\r\n]+)(\r|\n)+" +
+ @"Comment: *(?[^\r\n]+)(\r|\n)+" +
+ @"Public-Lines: *(?[0-9]+)(\r|\n)+" +
+ @"(?([a-zA-Z0-9/+=]{1,80}(\r|\n)+)+)" +
+ @"Private-Lines: *(?[0-9]+)(\r|\n)+" +
+ @"(?([a-zA-Z0-9/+=]{1,80}(\r|\n)+)+)" +
+ @"Private-(?(MAC|Hash)): *(?[a-zA-Z0-9/+=]+)",
#if FEATURE_REGEX_COMPILE
RegexOptions.Compiled | RegexOptions.Multiline);
#else
@@ -118,7 +134,6 @@ public PrivateKeyFile(Stream privateKey, string passPhrase)
///
/// 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)
@@ -126,17 +141,30 @@ private void Open(Stream privateKey, string passPhrase)
Match privateKeyMatch;
+ string text;
using (var sr = new StreamReader(privateKey))
+ text = sr.ReadToEnd();
+
+ privateKeyMatch = SshPrivateKeyRegex.Match(text);
+ if (privateKeyMatch.Success)
{
- var text = sr.ReadToEnd();
- privateKeyMatch = PrivateKeyRegex.Match(text);
+ SshOpen(passPhrase, privateKeyMatch);
+ return;
}
- if (!privateKeyMatch.Success)
+ privateKeyMatch = PuttyPrivateKeyRegex.Match(text);
+ if (privateKeyMatch.Success)
{
- throw new SshException("Invalid private key file.");
+ PuttyOpen(passPhrase, privateKeyMatch);
+ return;
}
+ throw new SshException("Invalid private key file.");
+ }
+
+ [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "this._key disposed in Dispose(bool) method.")]
+ private void SshOpen(string passPhrase, Match privateKeyMatch)
+ {
var keyName = privateKeyMatch.Result("${keyName}");
var cipherName = privateKeyMatch.Result("${cipherName}");
var salt = privateKeyMatch.Result("${salt}");
@@ -237,7 +265,7 @@ private void Open(Stream privateKey, string passPhrase)
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
@@ -274,6 +302,165 @@ private void Open(Stream privateKey, string passPhrase)
}
}
+ [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "this._key disposed in Dispose(bool) method.")]
+ private void PuttyOpen(string passPhrase, Match privateKeyMatch)
+ {
+ var fileVersion = Convert.ToInt32(privateKeyMatch.Result("${fileVersion}"));
+ var keyAlgo = privateKeyMatch.Result("${keyAlgo}");
+ var cipherName = privateKeyMatch.Result("${cipherName}");
+ var keyName = privateKeyMatch.Result("${keyName}");
+ var publicLines = Convert.ToInt32(privateKeyMatch.Result("${publicLines}"));
+ var publicData = privateKeyMatch.Result("${publicData}");
+ var privateLines = Convert.ToInt32(privateKeyMatch.Result("${privateLines}"));
+ var privateData = privateKeyMatch.Result("${privateData}");
+ var macOrHash = privateKeyMatch.Result("${macOrHash}");
+ var hashData = privateKeyMatch.Result("${hashData}");
+
+ if (fileVersion != 1 && fileVersion != 2)
+ throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "PuTTY private key file version {0} not supported.", fileVersion));
+
+ var publicDataBinary = Convert.FromBase64String(publicData);
+ var privateDataBinary = Convert.FromBase64String(privateData);
+
+ if (string.IsNullOrEmpty(cipherName))
+ throw new SshPassPhraseNullOrEmptyException("PuTTY private key file cipher name is invalid");
+
+ byte[] privateDataPlaintext;
+ if (cipherName == "none")
+ {
+ // Don't use a passphrase for unencrypted keys
+ passPhrase = "";
+ privateDataPlaintext = privateDataBinary;
+ }
+ else
+ {
+ if (string.IsNullOrEmpty(passPhrase))
+ throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
+
+ if (cipherName != "aes256-cbc")
+ throw new SshPassPhraseNullOrEmptyException(string.Format(CultureInfo.CurrentCulture, "Passphrase cipher '{0}' not supported.", cipherName));
+
+ CipherInfo cipherInfo = new CipherInfo(256, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
+ if (privateDataBinary.Length % 16 != 0)
+ throw new SshPassPhraseNullOrEmptyException("Private key data not multiple of cipher block size.");
+
+ var cipherKey = GetPuttyCipherKey(passPhrase, cipherInfo.KeySize / 8);
+ var cipher = cipherInfo.Cipher(cipherKey, new byte[cipherKey.Length]);
+
+ privateDataPlaintext = cipher.Decrypt(privateDataBinary);
+ }
+
+ byte[] macData;
+ if (fileVersion == 1)
+ {
+ // In old version, MAC/Hash only includes the private key
+ macData = privateDataPlaintext;
+ }
+ else
+ {
+ using (var data = new SshDataStream(0))
+ {
+ data.Write(keyAlgo, Encoding.UTF8);
+ data.Write(cipherName, Encoding.UTF8);
+ data.Write(keyName, Encoding.UTF8);
+ data.WriteBinary(publicDataBinary);
+ data.WriteBinary(privateDataPlaintext);
+ macData = data.ToArray();
+ }
+ }
+
+ byte[] macOrHashResult;
+ if (macOrHash == "MAC")
+ {
+ using (var sha1 = CryptoAbstraction.CreateSHA1())
+ {
+ byte[] macKey = sha1.ComputeHash(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key" + passPhrase));
+ using (var hmac = new HMACSHA1(macKey))
+ {
+ macOrHashResult = hmac.ComputeHash(macData);
+ }
+ }
+ }
+ else if (macOrHash == "Hash" && fileVersion == 1)
+ {
+ using (var sha1 = CryptoAbstraction.CreateSHA1())
+ {
+ macOrHashResult = sha1.ComputeHash(macData);
+ }
+ }
+ else
+ {
+ throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Private key verification algorithm {0} not supported for file version {1}", macOrHash, fileVersion));
+ }
+
+ if (!String.Equals(ConvertByteArrayToHex(macOrHashResult), hashData, StringComparison.OrdinalIgnoreCase))
+ {
+ throw new SshException("Invalid private key");
+ }
+
+ var reader = new SshDataReader(publicDataBinary);
+ var publicKeyAlgo = reader.ReadString(Encoding.UTF8);
+ if (publicKeyAlgo != keyAlgo)
+ {
+ throw new SshException(string.Format(CultureInfo.CurrentCulture, "Public key algorithm specified as {0}, expecting {1}", publicKeyAlgo, keyAlgo));
+ }
+ if (keyAlgo == "ssh-rsa")
+ {
+ var exponent = reader.ReadBigIntWithBytes();
+ var modulus = reader.ReadBigIntWithBytes();
+ reader = new SshDataReader(privateDataPlaintext);
+ var d = reader.ReadBigIntWithBytes();
+ var p = reader.ReadBigIntWithBytes();
+ var q = reader.ReadBigIntWithBytes();
+ var inverseQ = reader.ReadBigIntWithBytes();
+ _key = new RsaKey(modulus, exponent, d, p, q, inverseQ);
+ HostKey = new KeyHostAlgorithm("ssh-rsa", _key);
+ }
+ else if (keyAlgo == "ssh-dss")
+ {
+ var p = reader.ReadBigIntWithBytes();
+ var q = reader.ReadBigIntWithBytes();
+ var g = reader.ReadBigIntWithBytes();
+ var y = reader.ReadBigIntWithBytes();
+ reader = new SshDataReader(privateDataPlaintext);
+ var x = reader.ReadBigIntWithBytes();
+ _key = new DsaKey(p, q, g, y, x);
+ HostKey = new KeyHostAlgorithm("ssh-dss", _key);
+ }
+ else
+ {
+ throw new SshException(string.Format(CultureInfo.CurrentCulture, "Unsupported key algorithm {0}", keyAlgo));
+ }
+ }
+
+ private static string ConvertByteArrayToHex(byte[] bytes)
+ {
+ return bytes.Aggregate(new StringBuilder(bytes.Length * 2), (sb, b) => sb.Append(b.ToString("X2"))).ToString();
+ }
+
+ private static byte[] GetPuttyCipherKey(string passphrase, int length)
+ {
+ var cipherKey = new List();
+
+ using (var sha1 = CryptoAbstraction.CreateSHA1())
+ {
+ var passphraseBytes = Encoding.UTF8.GetBytes(passphrase);
+
+ int counter = 0;
+ do {
+ var counterBytes = BitConverter.GetBytes(counter++);
+
+ if (BitConverter.IsLittleEndian)
+ Array.Reverse(counterBytes);
+
+ var hash = sha1.ComputeHash(counterBytes.Concat(passphraseBytes).ToArray());
+ cipherKey.AddRange(hash);
+ } while (cipherKey.Count < length);
+ }
+
+ return cipherKey.Take(length).ToArray();
+ }
+
private static byte[] GetCipherKey(string passphrase, int length)
{
var cipherKey = new List();
@@ -409,6 +596,21 @@ public SshDataReader(byte[] data)
return base.ReadBytes(length);
}
+ ///
+ /// Reads next mpint data type from internal buffer where length specified in bytes.
+ ///
+ /// mpint read.
+ public BigInteger ReadBigIntWithBytes()
+ {
+ var length = (int)base.ReadUInt32();
+
+ 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().ToArray());
+ }
+
///
/// Reads next mpint data type from internal buffer where length specified in bits.
///