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. ///