PrivateKeyFile.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448
  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 Renci.SshNet.Common;
  9. using System.Globalization;
  10. using Renci.SshNet.Security.Cryptography;
  11. using Renci.SshNet.Security.Cryptography.Ciphers;
  12. using Renci.SshNet.Security.Cryptography.Ciphers.Modes;
  13. using Renci.SshNet.Security.Cryptography.Ciphers.Paddings;
  14. using System.Diagnostics.CodeAnalysis;
  15. namespace Renci.SshNet
  16. {
  17. /// <summary>
  18. /// Represents private key information.
  19. /// </summary>
  20. /// <example>
  21. /// <code source="..\..\Renci.SshNet.Tests\Data\Key.RSA.txt" language="Text" title="Private RSA key example" />
  22. /// </example>
  23. /// <remarks>
  24. /// <para>
  25. /// Supports RSA and DSA private key in both <c>OpenSSH</c> and <c>ssh.com</c> format.
  26. /// </para>
  27. /// <para>
  28. /// The following encryption algorithms are supported:
  29. /// <list type="bullet">
  30. /// <item>
  31. /// <description>DES-EDE3-CBC</description>
  32. /// </item>
  33. /// <item>
  34. /// <description>DES-EDE3-CFB</description>
  35. /// </item>
  36. /// <item>
  37. /// <description>DES-CBC</description>
  38. /// </item>
  39. /// <item>
  40. /// <description>AES-128-CBC</description>
  41. /// </item>
  42. /// <item>
  43. /// <description>AES-192-CBC</description>
  44. /// </item>
  45. /// <item>
  46. /// <description>AES-256-CBC</description>
  47. /// </item>
  48. /// </list>
  49. /// </para>
  50. /// </remarks>
  51. public class PrivateKeyFile : IDisposable
  52. {
  53. 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 *-+",
  54. #if SILVERLIGHT
  55. RegexOptions.Multiline);
  56. #else
  57. RegexOptions.Compiled | RegexOptions.Multiline);
  58. #endif
  59. private Key _key;
  60. /// <summary>
  61. /// Gets the host key.
  62. /// </summary>
  63. public HostAlgorithm HostKey { get; private set; }
  64. /// <summary>
  65. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  66. /// </summary>
  67. /// <param name="privateKey">The private key.</param>
  68. public PrivateKeyFile(Stream privateKey)
  69. {
  70. this.Open(privateKey, null);
  71. }
  72. /// <summary>
  73. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  74. /// </summary>
  75. /// <param name="fileName">Name of the file.</param>
  76. /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is null or empty.</exception>
  77. /// <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>
  78. public PrivateKeyFile(string fileName)
  79. : this(fileName, null)
  80. {
  81. }
  82. /// <summary>
  83. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  84. /// </summary>
  85. /// <param name="fileName">Name of the file.</param>
  86. /// <param name="passPhrase">The pass phrase.</param>
  87. /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is null or empty, or <paramref name="passPhrase"/> is null.</exception>
  88. /// <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>
  89. public PrivateKeyFile(string fileName, string passPhrase)
  90. {
  91. if (string.IsNullOrEmpty(fileName))
  92. throw new ArgumentNullException("fileName");
  93. using (var keyFile = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
  94. {
  95. this.Open(keyFile, passPhrase);
  96. }
  97. }
  98. /// <summary>
  99. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  100. /// </summary>
  101. /// <param name="privateKey">The private key.</param>
  102. /// <param name="passPhrase">The pass phrase.</param>
  103. /// <exception cref="ArgumentNullException"><paramref name="privateKey"/> or <paramref name="passPhrase"/> is null.</exception>
  104. public PrivateKeyFile(Stream privateKey, string passPhrase)
  105. {
  106. this.Open(privateKey, passPhrase);
  107. }
  108. /// <summary>
  109. /// Opens the specified private key.
  110. /// </summary>
  111. /// <param name="privateKey">The private key.</param>
  112. /// <param name="passPhrase">The pass phrase.</param>
  113. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "this._key disposed in Dispose(bool) method.")]
  114. private void Open(Stream privateKey, string passPhrase)
  115. {
  116. if (privateKey == null)
  117. throw new ArgumentNullException("privateKey");
  118. Match privateKeyMatch;
  119. using (var sr = new StreamReader(privateKey))
  120. {
  121. var text = sr.ReadToEnd();
  122. privateKeyMatch = PrivateKeyRegex.Match(text);
  123. }
  124. if (!privateKeyMatch.Success)
  125. {
  126. throw new SshException("Invalid private key file.");
  127. }
  128. var keyName = privateKeyMatch.Result("${keyName}");
  129. var cipherName = privateKeyMatch.Result("${cipherName}");
  130. var salt = privateKeyMatch.Result("${salt}");
  131. var data = privateKeyMatch.Result("${data}");
  132. var binaryData = Convert.FromBase64String(data);
  133. byte[] decryptedData;
  134. if (!string.IsNullOrEmpty(cipherName) && !string.IsNullOrEmpty(salt))
  135. {
  136. if (string.IsNullOrEmpty(passPhrase))
  137. throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
  138. var binarySalt = new byte[salt.Length / 2];
  139. for (var i = 0; i < binarySalt.Length; i++)
  140. binarySalt[i] = Convert.ToByte(salt.Substring(i * 2, 2), 16);
  141. CipherInfo cipher;
  142. switch (cipherName)
  143. {
  144. case "DES-EDE3-CBC":
  145. cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
  146. break;
  147. case "DES-EDE3-CFB":
  148. cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CfbCipherMode(iv), new PKCS7Padding()));
  149. break;
  150. case "DES-CBC":
  151. cipher = new CipherInfo(64, (key, iv) => new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
  152. break;
  153. case "AES-128-CBC":
  154. cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
  155. break;
  156. case "AES-192-CBC":
  157. cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
  158. break;
  159. case "AES-256-CBC":
  160. cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
  161. break;
  162. default:
  163. throw new SshException(string.Format(CultureInfo.CurrentCulture, "Private key cipher \"{0}\" is not supported.", cipherName));
  164. }
  165. decryptedData = DecryptKey(cipher, binaryData, passPhrase, binarySalt);
  166. }
  167. else
  168. {
  169. decryptedData = binaryData;
  170. }
  171. switch (keyName)
  172. {
  173. case "RSA":
  174. this._key = new RsaKey(decryptedData.ToArray());
  175. this.HostKey = new KeyHostAlgorithm("ssh-rsa", this._key);
  176. break;
  177. case "DSA":
  178. this._key = new DsaKey(decryptedData.ToArray());
  179. this.HostKey = new KeyHostAlgorithm("ssh-dss", this._key);
  180. break;
  181. case "SSH2 ENCRYPTED":
  182. var reader = new SshDataReader(decryptedData);
  183. var magicNumber = reader.ReadUInt32();
  184. if (magicNumber != 0x3f6ff9eb)
  185. {
  186. throw new SshException("Invalid SSH2 private key.");
  187. }
  188. reader.ReadUInt32(); // Read total bytes length including magic number
  189. var keyType = reader.ReadString();
  190. var ssh2CipherName = reader.ReadString();
  191. var blobSize = (int)reader.ReadUInt32();
  192. byte[] keyData;
  193. if (ssh2CipherName == "none")
  194. {
  195. keyData = reader.ReadBytes(blobSize);
  196. }
  197. else if (ssh2CipherName == "3des-cbc")
  198. {
  199. if (string.IsNullOrEmpty(passPhrase))
  200. throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
  201. var key = GetCipherKey(passPhrase, 192 / 8);
  202. var ssh2Сipher = new TripleDesCipher(key, new CbcCipherMode(new byte[8]), new PKCS7Padding());
  203. keyData = ssh2Сipher.Decrypt(reader.ReadBytes(blobSize));
  204. }
  205. else
  206. {
  207. throw new SshException(string.Format("Cipher method '{0}' is not supported.", cipherName));
  208. }
  209. // TODO: Create two specific data types to avoid using SshDataReader class
  210. reader = new SshDataReader(keyData);
  211. var decryptedLength = reader.ReadUInt32();
  212. if (decryptedLength > blobSize - 4)
  213. throw new SshException("Invalid passphrase.");
  214. if (keyType == "if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}")
  215. {
  216. var exponent = reader.ReadBigIntWithBits();//e
  217. var d = reader.ReadBigIntWithBits();//d
  218. var modulus = reader.ReadBigIntWithBits();//n
  219. var inverseQ = reader.ReadBigIntWithBits();//u
  220. var q = reader.ReadBigIntWithBits();//p
  221. var p = reader.ReadBigIntWithBits();//q
  222. this._key = new RsaKey(modulus, exponent, d, p, q, inverseQ);
  223. this.HostKey = new KeyHostAlgorithm("ssh-rsa", this._key);
  224. }
  225. else if (keyType == "dl-modp{sign{dsa-nist-sha1},dh{plain}}")
  226. {
  227. var zero = reader.ReadUInt32();
  228. if (zero != 0)
  229. {
  230. throw new SshException("Invalid private key");
  231. }
  232. var p = reader.ReadBigIntWithBits();
  233. var g = reader.ReadBigIntWithBits();
  234. var q = reader.ReadBigIntWithBits();
  235. var y = reader.ReadBigIntWithBits();
  236. var x = reader.ReadBigIntWithBits();
  237. this._key = new DsaKey(p, q, g, y, x);
  238. this.HostKey = new KeyHostAlgorithm("ssh-dss", this._key);
  239. }
  240. else
  241. {
  242. throw new NotSupportedException(string.Format("Key type '{0}' is not supported.", keyType));
  243. }
  244. break;
  245. default:
  246. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", keyName));
  247. }
  248. }
  249. private static byte[] GetCipherKey(string passphrase, int length)
  250. {
  251. var cipherKey = new List<byte>();
  252. using (var md5 = new MD5Hash())
  253. {
  254. var passwordBytes = Encoding.UTF8.GetBytes(passphrase);
  255. var hash = md5.ComputeHash(passwordBytes);
  256. cipherKey.AddRange(hash);
  257. while (cipherKey.Count < length)
  258. {
  259. hash = passwordBytes.Concat(hash).ToArray();
  260. hash = md5.ComputeHash(hash);
  261. cipherKey.AddRange(hash);
  262. }
  263. }
  264. return cipherKey.Take(length).ToArray();
  265. }
  266. /// <summary>
  267. /// Decrypts encrypted private key file data.
  268. /// </summary>
  269. /// <param name="cipherInfo">The cipher info.</param>
  270. /// <param name="cipherData">Encrypted data.</param>
  271. /// <param name="passPhrase">Decryption pass phrase.</param>
  272. /// <param name="binarySalt">Decryption binary salt.</param>
  273. /// <returns>Decrypted byte array.</returns>
  274. /// <exception cref="System.ArgumentNullException">cipherInfo</exception>
  275. /// <exception cref="ArgumentNullException"><paramref name="cipherInfo" />, <paramref name="cipherData" />, <paramref name="passPhrase" /> or <paramref name="binarySalt" /> is null.</exception>
  276. private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt)
  277. {
  278. if (cipherInfo == null)
  279. throw new ArgumentNullException("cipherInfo");
  280. if (cipherData == null)
  281. throw new ArgumentNullException("cipherData");
  282. if (binarySalt == null)
  283. throw new ArgumentNullException("binarySalt");
  284. var cipherKey = new List<byte>();
  285. using (var md5 = new MD5Hash())
  286. {
  287. var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
  288. // Use 8 bytes binary salt
  289. var initVector = passwordBytes.Concat(binarySalt.Take(8)).ToArray();
  290. var hash = md5.ComputeHash(initVector);
  291. cipherKey.AddRange(hash);
  292. while (cipherKey.Count < cipherInfo.KeySize / 8)
  293. {
  294. hash = hash.Concat(initVector).ToArray();
  295. hash = md5.ComputeHash(hash);
  296. cipherKey.AddRange(hash);
  297. }
  298. }
  299. var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
  300. return cipher.Decrypt(cipherData);
  301. }
  302. #region IDisposable Members
  303. private bool _isDisposed;
  304. /// <summary>
  305. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged ResourceMessages.
  306. /// </summary>
  307. public void Dispose()
  308. {
  309. Dispose(true);
  310. GC.SuppressFinalize(this);
  311. }
  312. /// <summary>
  313. /// Releases unmanaged and - optionally - managed resources
  314. /// </summary>
  315. /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged ResourceMessages.</param>
  316. protected virtual void Dispose(bool disposing)
  317. {
  318. // Check to see if Dispose has already been called.
  319. if (!this._isDisposed)
  320. {
  321. // If disposing equals true, dispose all managed
  322. // and unmanaged ResourceMessages.
  323. if (disposing)
  324. {
  325. // Dispose managed ResourceMessages.
  326. if (this._key != null)
  327. {
  328. ((IDisposable)this._key).Dispose();
  329. this._key = null;
  330. }
  331. }
  332. // Note disposing has been done.
  333. _isDisposed = true;
  334. }
  335. }
  336. /// <summary>
  337. /// Releases unmanaged resources and performs other cleanup operations before the
  338. /// <see cref="BaseClient"/> is reclaimed by garbage collection.
  339. /// </summary>
  340. ~PrivateKeyFile()
  341. {
  342. // Do not re-create Dispose clean-up code here.
  343. // Calling Dispose(false) is optimal in terms of
  344. // readability and maintainability.
  345. Dispose(false);
  346. }
  347. #endregion
  348. private class SshDataReader : SshData
  349. {
  350. public SshDataReader(byte[] data)
  351. {
  352. this.LoadBytes(data);
  353. }
  354. public new UInt32 ReadUInt32()
  355. {
  356. return base.ReadUInt32();
  357. }
  358. public new string ReadString()
  359. {
  360. return base.ReadString();
  361. }
  362. public new byte[] ReadBytes(int length)
  363. {
  364. return base.ReadBytes(length);
  365. }
  366. /// <summary>
  367. /// Reads next mpint data type from internal buffer where length specified in bits.
  368. /// </summary>
  369. /// <returns>mpint read.</returns>
  370. public BigInteger ReadBigIntWithBits()
  371. {
  372. var length = (int)base.ReadUInt32();
  373. length = (length + 7) / 8;
  374. var data = base.ReadBytes(length);
  375. var bytesArray = new byte[data.Length + 1];
  376. Buffer.BlockCopy(data, 0, bytesArray, 1, data.Length);
  377. return new BigInteger(bytesArray.Reverse().ToArray());
  378. }
  379. protected override void LoadData()
  380. {
  381. }
  382. protected override void SaveData()
  383. {
  384. }
  385. }
  386. }
  387. }