Skip to content

Commit

Permalink
Add support for PuTTY ppk format private key files
Browse files Browse the repository at this point in the history
  • Loading branch information
rmini committed Sep 16, 2018
1 parent bd01d97 commit 0cfd353
Showing 1 changed file with 210 additions and 8 deletions.
218 changes: 210 additions & 8 deletions src/Renci.SshNet/PrivateKeyFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -22,7 +24,7 @@ namespace Renci.SshNet
/// </example>
/// <remarks>
/// <para>
/// Supports RSA and DSA private key in both <c>OpenSSH</c> and <c>ssh.com</c> format.
/// Supports RSA and DSA private key in <c>OpenSSH</c>, <c>ssh.com</c>, and <c>PuTTY</c> formats.
/// </para>
/// <para>
/// The following encryption algorithms are supported:
Expand Down Expand Up @@ -50,7 +52,21 @@ namespace Renci.SshNet
/// </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 *-+",
private static readonly Regex SshPrivateKeyRegex = 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 static readonly Regex PuttyPrivateKeyRegex = new Regex(
@"^PuTTY-User-Key-File-(?<fileVersion>[0-9]+): *(?<keyAlgo>[^\r\n]+)(\r|\n)+" +
@"Encryption: *(?<cipherName>[^\r\n]+)(\r|\n)+" +
@"Comment: *(?<keyName>[^\r\n]+)(\r|\n)+" +
@"Public-Lines: *(?<publicLines>[0-9]+)(\r|\n)+" +
@"(?<publicData>([a-zA-Z0-9/+=]{1,80}(\r|\n)+)+)" +
@"Private-Lines: *(?<privateLines>[0-9]+)(\r|\n)+" +
@"(?<privateData>([a-zA-Z0-9/+=]{1,80}(\r|\n)+)+)" +
@"Private-(?<macOrHash>(MAC|Hash)): *(?<hashData>[a-zA-Z0-9/+=]+)",
#if FEATURE_REGEX_COMPILE
RegexOptions.Compiled | RegexOptions.Multiline);
#else
Expand Down Expand Up @@ -118,25 +134,37 @@ public PrivateKeyFile(Stream privateKey, string passPhrase)
/// </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;

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}");
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<byte>();

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<byte>();
Expand Down Expand Up @@ -409,6 +596,21 @@ public SshDataReader(byte[] data)
return base.ReadBytes(length);
}

/// <summary>
/// Reads next mpint data type from internal buffer where length specified in bytes.
/// </summary>
/// <returns>mpint read.</returns>
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());
}

/// <summary>
/// Reads next mpint data type from internal buffer where length specified in bits.
/// </summary>
Expand Down

0 comments on commit 0cfd353

Please sign in to comment.