PrivateKeyFile.cs 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. using System;
  2. using System.Linq;
  3. using System.Collections.Generic;
  4. using System.IO;
  5. using System.Text;
  6. using System.Text.RegularExpressions;
  7. using Renci.SshNet.Security;
  8. using System.Security.Cryptography;
  9. using System.Security;
  10. using Renci.SshNet.Common;
  11. using System.Globalization;
  12. using Renci.SshNet.Security.Cryptography;
  13. using Renci.SshNet.Security.Cryptography.Ciphers;
  14. using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
  15. using Renci.SshNet.Security.Cryptography.Ciphers.Paddings;
  16. namespace Renci.SshNet
  17. {
  18. /// <summary>
  19. /// old private key information/
  20. /// </summary>
  21. public class PrivateKeyFile
  22. {
  23. #if SILVERLIGHT
  24. private static Regex _privateKeyRegex = new Regex(@"^-----BEGIN (?<keyName>\w+) PRIVATE KEY-----\r?\n(Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]{16})\r?\n\r?\n)?(?<data>([a-zA-Z0-9/+=]{1,64}\r?\n)+)-----END \k<keyName> PRIVATE KEY-----.*", RegexOptions.Multiline);
  25. #else
  26. private static Regex _privateKeyRegex = new Regex(@"^-----BEGIN (?<keyName>\w+) PRIVATE KEY-----\r?\n(Proc-Type: 4,ENCRYPTED\r?\nDEK-Info: (?<cipherName>[A-Z0-9-]+),(?<salt>[A-F0-9]{16})\r?\n\r?\n)?(?<data>([a-zA-Z0-9/+=]{1,64}\r?\n)+)-----END \k<keyName> PRIVATE KEY-----.*", RegexOptions.Compiled | RegexOptions.Multiline);
  27. #endif
  28. /// <summary>
  29. /// Gets the host key.
  30. /// </summary>
  31. public HostAlgorithm HostKey;
  32. /// <summary>
  33. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  34. /// </summary>
  35. /// <param name="privateKey">The private key.</param>
  36. public PrivateKeyFile(Stream privateKey)
  37. {
  38. this.Open(privateKey, null);
  39. }
  40. /// <summary>
  41. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  42. /// </summary>
  43. /// <param name="fileName">Name of the file.</param>
  44. /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is null or empty.</exception>
  45. /// <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>
  46. public PrivateKeyFile(string fileName)
  47. {
  48. if (string.IsNullOrEmpty(fileName))
  49. throw new ArgumentNullException("fileName");
  50. using (var keyFile = File.Open(fileName, FileMode.Open))
  51. {
  52. this.Open(keyFile, null);
  53. }
  54. }
  55. /// <summary>
  56. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  57. /// </summary>
  58. /// <param name="fileName">Name of the file.</param>
  59. /// <param name="passPhrase">The pass phrase.</param>
  60. /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is null or empty, or <paramref name="passPhrase"/> is null.</exception>
  61. /// <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>
  62. public PrivateKeyFile(string fileName, string passPhrase)
  63. {
  64. if (string.IsNullOrEmpty(fileName))
  65. throw new ArgumentNullException("fileName");
  66. using (var keyFile = File.Open(fileName, FileMode.Open))
  67. {
  68. this.Open(keyFile, passPhrase);
  69. }
  70. }
  71. /// <summary>
  72. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  73. /// </summary>
  74. /// <param name="privateKey">The private key.</param>
  75. /// <param name="passPhrase">The pass phrase.</param>
  76. /// <exception cref="ArgumentNullException"><paramref name="privateKey"/> or <paramref name="passPhrase"/> is null.</exception>
  77. public PrivateKeyFile(Stream privateKey, string passPhrase)
  78. {
  79. this.Open(privateKey, passPhrase);
  80. }
  81. /// <summary>
  82. /// Opens the specified private key.
  83. /// </summary>
  84. /// <param name="privateKey">The private key.</param>
  85. /// <param name="passPhrase">The pass phrase.</param>
  86. private void Open(Stream privateKey, string passPhrase)
  87. {
  88. if (privateKey == null)
  89. throw new ArgumentNullException("privateKey");
  90. Match privateKeyMatch = null;
  91. using (StreamReader sr = new StreamReader(privateKey))
  92. {
  93. var text = sr.ReadToEnd();
  94. privateKeyMatch = _privateKeyRegex.Match(text);
  95. }
  96. if (!privateKeyMatch.Success)
  97. {
  98. throw new SshException("Invalid private key file.");
  99. }
  100. var keyName = privateKeyMatch.Result("${keyName}");
  101. var cipherName = privateKeyMatch.Result("${cipherName}");
  102. var salt = privateKeyMatch.Result("${salt}");
  103. var data = privateKeyMatch.Result("${data}");
  104. var binaryData = System.Convert.FromBase64String(data);
  105. IEnumerable<byte> decryptedData;
  106. if (!string.IsNullOrEmpty(cipherName) && !string.IsNullOrEmpty(salt))
  107. {
  108. if (string.IsNullOrEmpty(passPhrase))
  109. throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
  110. byte[] binarySalt = new byte[salt.Length / 2];
  111. for (int i = 0; i < binarySalt.Length; i++)
  112. binarySalt[i] = Convert.ToByte(salt.Substring(i * 2, 2), 16);
  113. CipherInfo cipher = null;
  114. switch (cipherName)
  115. {
  116. case "DES-EDE3-CBC":
  117. cipher = new CipherInfo(192, (key, iv) => { return new TripleDesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); });
  118. break;
  119. case "DES-EDE3-CFB":
  120. cipher = new CipherInfo(192, (key, iv) => { return new TripleDesCipher(key, new CfbCipherMode(iv), new PKCS7Padding()); });
  121. break;
  122. case "DES-CBC":
  123. // TODO: Not tested
  124. cipher = new CipherInfo(64, (key, iv) => { return new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); });
  125. break;
  126. case "AES-128-CBC":
  127. // TODO: Not tested
  128. cipher = new CipherInfo(128, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); });
  129. break;
  130. case "AES-192-CBC":
  131. // TODO: Not tested
  132. cipher = new CipherInfo(192, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); });
  133. break;
  134. case "AES-256-CBC":
  135. // TODO: Not tested
  136. cipher = new CipherInfo(256, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); });
  137. break;
  138. default:
  139. throw new SshException(string.Format(CultureInfo.CurrentCulture, "Unknown private key cipher \"{0}\".", cipherName));
  140. }
  141. decryptedData = DecryptKey(cipher, binaryData, passPhrase, binarySalt);
  142. }
  143. else
  144. {
  145. decryptedData = binaryData;
  146. }
  147. switch (keyName)
  148. {
  149. case "RSA":
  150. this.HostKey = new KeyHostAlgorithm("ssh-rsa", new RsaKey(decryptedData.ToArray()));
  151. break;
  152. case "DSA":
  153. this.HostKey = new KeyHostAlgorithm("ssh-dss", new DsaKey(decryptedData.ToArray()));
  154. break;
  155. default:
  156. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", keyName));
  157. }
  158. }
  159. /// <summary>
  160. /// Decrypts encrypted private key file data.
  161. /// </summary>
  162. /// <param name="cipherInfo">The cipher info.</param>
  163. /// <param name="cipherData">Encrypted data.</param>
  164. /// <param name="passPhrase">Decryption pass phrase.</param>
  165. /// <param name="binarySalt">Decryption binary salt.</param>
  166. /// <returns></returns>
  167. /// <exception cref="ArgumentNullException"><paramref name="cipherInfo"/>, <paramref name="cipherData"/>, <paramref name="passPhrase"/> or <paramref name="binarySalt"/> is null.</exception>
  168. public static IEnumerable<byte> DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt)
  169. {
  170. if (cipherInfo == null)
  171. throw new ArgumentNullException("cipherInfo");
  172. if (cipherData == null)
  173. throw new ArgumentNullException("cipherData");
  174. if (binarySalt == null)
  175. throw new ArgumentNullException("binarySalt");
  176. List<byte> cipherKey = new List<byte>();
  177. using (var md5 = new MD5Hash())
  178. {
  179. var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
  180. var initVector = passwordBytes.Concat(binarySalt);
  181. var hash = md5.ComputeHash(initVector.ToArray()).AsEnumerable();
  182. cipherKey.AddRange(hash);
  183. while (cipherKey.Count < cipherInfo.KeySize / 8)
  184. {
  185. hash = hash.Concat(initVector);
  186. hash = md5.ComputeHash(hash.ToArray());
  187. cipherKey.AddRange(hash);
  188. }
  189. }
  190. var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
  191. return cipher.Decrypt(cipherData);
  192. }
  193. }
  194. }