PrivateKeyFile.cs 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  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]+)\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]+)\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. 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. cipher = new CipherInfo(64, (key, iv) => { return new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); });
  124. break;
  125. // TODO: Implement more private key ciphers
  126. //case "AES-128-CBC":
  127. // cipher = new CipherInfo(128, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); });
  128. // break;
  129. //case "AES-192-CBC":
  130. // cipher = new CipherInfo(192, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); });
  131. // break;
  132. //case "AES-256-CBC":
  133. // cipher = new CipherInfo(256, (key, iv) => { return new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()); });
  134. // break;
  135. default:
  136. throw new SshException(string.Format(CultureInfo.CurrentCulture, "Private key cipher \"{0}\" is not supported.", cipherName));
  137. }
  138. decryptedData = DecryptKey(cipher, binaryData, passPhrase, binarySalt);
  139. }
  140. else
  141. {
  142. decryptedData = binaryData;
  143. }
  144. switch (keyName)
  145. {
  146. case "RSA":
  147. this.HostKey = new KeyHostAlgorithm("ssh-rsa", new RsaKey(decryptedData.ToArray()));
  148. break;
  149. case "DSA":
  150. this.HostKey = new KeyHostAlgorithm("ssh-dss", new DsaKey(decryptedData.ToArray()));
  151. break;
  152. default:
  153. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", keyName));
  154. }
  155. }
  156. /// <summary>
  157. /// Decrypts encrypted private key file data.
  158. /// </summary>
  159. /// <param name="cipherInfo">The cipher info.</param>
  160. /// <param name="cipherData">Encrypted data.</param>
  161. /// <param name="passPhrase">Decryption pass phrase.</param>
  162. /// <param name="binarySalt">Decryption binary salt.</param>
  163. /// <returns></returns>
  164. /// <exception cref="ArgumentNullException"><paramref name="cipherInfo"/>, <paramref name="cipherData"/>, <paramref name="passPhrase"/> or <paramref name="binarySalt"/> is null.</exception>
  165. public static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt)
  166. {
  167. if (cipherInfo == null)
  168. throw new ArgumentNullException("cipherInfo");
  169. if (cipherData == null)
  170. throw new ArgumentNullException("cipherData");
  171. if (binarySalt == null)
  172. throw new ArgumentNullException("binarySalt");
  173. List<byte> cipherKey = new List<byte>();
  174. using (var md5 = new MD5Hash())
  175. {
  176. var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
  177. var initVector = passwordBytes.Concat(binarySalt);
  178. var hash = md5.ComputeHash(initVector.ToArray()).AsEnumerable();
  179. cipherKey.AddRange(hash);
  180. while (cipherKey.Count < cipherInfo.KeySize / 8)
  181. {
  182. hash = hash.Concat(initVector);
  183. hash = md5.ComputeHash(hash.ToArray());
  184. cipherKey.AddRange(hash);
  185. }
  186. }
  187. var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
  188. return cipher.Decrypt(cipherData);
  189. }
  190. }
  191. }