PrivateKeyFile.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.Globalization;
  5. using System.IO;
  6. using System.Security.Cryptography;
  7. using System.Text;
  8. using System.Text.RegularExpressions;
  9. using Renci.SshNet.Abstractions;
  10. using Renci.SshNet.Common;
  11. using Renci.SshNet.Security;
  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. /// Represents private key information.
  20. /// </summary>
  21. /// <example>
  22. /// <code source="..\..\src\Renci.SshNet.Tests\Data\Key.RSA.txt" language="Text" title="Private RSA key example" />
  23. /// </example>
  24. /// <remarks>
  25. /// <para>
  26. /// The following private keys are supported:
  27. /// <list type="bullet">
  28. /// <item>
  29. /// <description>RSA in OpenSSL PEM, ssh.com and OpenSSH key format</description>
  30. /// </item>
  31. /// <item>
  32. /// <description>DSA in OpenSSL PEM and ssh.com format</description>
  33. /// </item>
  34. /// <item>
  35. /// <description>ECDSA 256/384/521 in OpenSSL PEM and OpenSSH key format</description>
  36. /// </item>
  37. /// <item>
  38. /// <description>ED25519 in OpenSSH key format</description>
  39. /// </item>
  40. /// </list>
  41. /// </para>
  42. /// <para>
  43. /// The following encryption algorithms are supported:
  44. /// <list type="bullet">
  45. /// <item>
  46. /// <description>DES-EDE3-CBC</description>
  47. /// </item>
  48. /// <item>
  49. /// <description>DES-EDE3-CFB</description>
  50. /// </item>
  51. /// <item>
  52. /// <description>DES-CBC</description>
  53. /// </item>
  54. /// <item>
  55. /// <description>AES-128-CBC</description>
  56. /// </item>
  57. /// <item>
  58. /// <description>AES-192-CBC</description>
  59. /// </item>
  60. /// <item>
  61. /// <description>AES-256-CBC</description>
  62. /// </item>
  63. /// </list>
  64. /// </para>
  65. /// </remarks>
  66. public class PrivateKeyFile : IPrivateKeySource, IDisposable
  67. {
  68. 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 *-+",
  69. RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.ExplicitCapture);
  70. private readonly List<HostAlgorithm> _hostAlgorithms = new List<HostAlgorithm>();
  71. private Key _key;
  72. private bool _isDisposed;
  73. /// <summary>
  74. /// Gets the supported host algorithms for this key file.
  75. /// </summary>
  76. public IReadOnlyCollection<HostAlgorithm> HostKeyAlgorithms
  77. {
  78. get
  79. {
  80. return _hostAlgorithms;
  81. }
  82. }
  83. /// <summary>
  84. /// Gets the key.
  85. /// </summary>
  86. public Key Key
  87. {
  88. get
  89. {
  90. return _key;
  91. }
  92. }
  93. /// <summary>
  94. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  95. /// </summary>
  96. /// <param name="key">The key.</param>
  97. public PrivateKeyFile(Key key)
  98. {
  99. _key = key;
  100. _hostAlgorithms.Add(new KeyHostAlgorithm(key.ToString(), key));
  101. }
  102. /// <summary>
  103. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  104. /// </summary>
  105. /// <param name="privateKey">The private key.</param>
  106. public PrivateKeyFile(Stream privateKey)
  107. {
  108. Open(privateKey, passPhrase: null);
  109. Debug.Assert(_hostAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
  110. }
  111. /// <summary>
  112. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  113. /// </summary>
  114. /// <param name="fileName">Name of the file.</param>
  115. /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <see langword="null"/> or empty.</exception>
  116. /// <remarks>
  117. /// This method calls <see cref="File.Open(string, FileMode)"/> internally, this method does not catch exceptions from <see cref="File.Open(string, FileMode)"/>.
  118. /// </remarks>
  119. public PrivateKeyFile(string fileName)
  120. : this(fileName, passPhrase: null)
  121. {
  122. }
  123. /// <summary>
  124. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  125. /// </summary>
  126. /// <param name="fileName">Name of the file.</param>
  127. /// <param name="passPhrase">The pass phrase.</param>
  128. /// <exception cref="ArgumentNullException"><paramref name="fileName"/> is <see langword="null"/> or empty, or <paramref name="passPhrase"/> is <see langword="null"/>.</exception>
  129. /// <remarks>
  130. /// This method calls <see cref="File.Open(string, FileMode)"/> internally, this method does not catch exceptions from <see cref="File.Open(string, FileMode)"/>.
  131. /// </remarks>
  132. public PrivateKeyFile(string fileName, string passPhrase)
  133. {
  134. if (string.IsNullOrEmpty(fileName))
  135. {
  136. throw new ArgumentNullException(nameof(fileName));
  137. }
  138. using (var keyFile = File.Open(fileName, FileMode.Open, FileAccess.Read, FileShare.Read))
  139. {
  140. Open(keyFile, passPhrase);
  141. }
  142. Debug.Assert(_hostAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
  143. }
  144. /// <summary>
  145. /// Initializes a new instance of the <see cref="PrivateKeyFile"/> class.
  146. /// </summary>
  147. /// <param name="privateKey">The private key.</param>
  148. /// <param name="passPhrase">The pass phrase.</param>
  149. /// <exception cref="ArgumentNullException"><paramref name="privateKey"/> or <paramref name="passPhrase"/> is <see langword="null"/>.</exception>
  150. public PrivateKeyFile(Stream privateKey, string passPhrase)
  151. {
  152. Open(privateKey, passPhrase);
  153. Debug.Assert(_hostAlgorithms.Count > 0, $"{nameof(HostKeyAlgorithms)} is not set.");
  154. }
  155. /// <summary>
  156. /// Opens the specified private key.
  157. /// </summary>
  158. /// <param name="privateKey">The private key.</param>
  159. /// <param name="passPhrase">The pass phrase.</param>
  160. private void Open(Stream privateKey, string passPhrase)
  161. {
  162. if (privateKey is null)
  163. {
  164. throw new ArgumentNullException(nameof(privateKey));
  165. }
  166. Match privateKeyMatch;
  167. using (var sr = new StreamReader(privateKey))
  168. {
  169. var text = sr.ReadToEnd();
  170. privateKeyMatch = PrivateKeyRegex.Match(text);
  171. }
  172. if (!privateKeyMatch.Success)
  173. {
  174. throw new SshException("Invalid private key file.");
  175. }
  176. var keyName = privateKeyMatch.Result("${keyName}");
  177. var cipherName = privateKeyMatch.Result("${cipherName}");
  178. var salt = privateKeyMatch.Result("${salt}");
  179. var data = privateKeyMatch.Result("${data}");
  180. var binaryData = Convert.FromBase64String(data);
  181. byte[] decryptedData;
  182. if (!string.IsNullOrEmpty(cipherName) && !string.IsNullOrEmpty(salt))
  183. {
  184. if (string.IsNullOrEmpty(passPhrase))
  185. {
  186. throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
  187. }
  188. var binarySalt = new byte[salt.Length / 2];
  189. for (var i = 0; i < binarySalt.Length; i++)
  190. {
  191. binarySalt[i] = Convert.ToByte(salt.Substring(i * 2, 2), 16);
  192. }
  193. CipherInfo cipher;
  194. switch (cipherName)
  195. {
  196. case "DES-EDE3-CBC":
  197. cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
  198. break;
  199. case "DES-EDE3-CFB":
  200. cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, new CfbCipherMode(iv), new PKCS7Padding()));
  201. break;
  202. case "DES-CBC":
  203. cipher = new CipherInfo(64, (key, iv) => new DesCipher(key, new CbcCipherMode(iv), new PKCS7Padding()));
  204. break;
  205. case "AES-128-CBC":
  206. cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
  207. break;
  208. case "AES-192-CBC":
  209. cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
  210. break;
  211. case "AES-256-CBC":
  212. cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
  213. break;
  214. default:
  215. throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key cipher \"{0}\" is not supported.", cipherName));
  216. }
  217. decryptedData = DecryptKey(cipher, binaryData, passPhrase, binarySalt);
  218. }
  219. else
  220. {
  221. decryptedData = binaryData;
  222. }
  223. switch (keyName)
  224. {
  225. case "RSA":
  226. var rsaKey = new RsaKey(decryptedData);
  227. _key = rsaKey;
  228. #pragma warning disable CA2000 // Dispose objects before losing scope
  229. _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA512)));
  230. _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(rsaKey, HashAlgorithmName.SHA256)));
  231. #pragma warning restore CA2000 // Dispose objects before losing scope
  232. _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
  233. break;
  234. case "DSA":
  235. _key = new DsaKey(decryptedData);
  236. _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
  237. break;
  238. case "EC":
  239. _key = new EcdsaKey(decryptedData);
  240. _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
  241. break;
  242. case "OPENSSH":
  243. _key = ParseOpenSshV1Key(decryptedData, passPhrase);
  244. if (_key is RsaKey parsedRsaKey)
  245. {
  246. #pragma warning disable CA2000 // Dispose objects before losing scope
  247. _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(parsedRsaKey, HashAlgorithmName.SHA512)));
  248. _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(parsedRsaKey, HashAlgorithmName.SHA256)));
  249. #pragma warning restore CA2000 // Dispose objects before losing scope
  250. _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
  251. }
  252. else
  253. {
  254. _hostAlgorithms.Add(new KeyHostAlgorithm(_key.ToString(), _key));
  255. }
  256. break;
  257. case "SSH2 ENCRYPTED":
  258. var reader = new SshDataReader(decryptedData);
  259. var magicNumber = reader.ReadUInt32();
  260. if (magicNumber != 0x3f6ff9eb)
  261. {
  262. throw new SshException("Invalid SSH2 private key.");
  263. }
  264. _ = reader.ReadUInt32(); // Read total bytes length including magic number
  265. var keyType = reader.ReadString(SshData.Ascii);
  266. var ssh2CipherName = reader.ReadString(SshData.Ascii);
  267. var blobSize = (int)reader.ReadUInt32();
  268. byte[] keyData;
  269. if (ssh2CipherName == "none")
  270. {
  271. keyData = reader.ReadBytes(blobSize);
  272. }
  273. else if (ssh2CipherName == "3des-cbc")
  274. {
  275. if (string.IsNullOrEmpty(passPhrase))
  276. {
  277. throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
  278. }
  279. var key = GetCipherKey(passPhrase, 192 / 8);
  280. var ssh2Сipher = new TripleDesCipher(key, new CbcCipherMode(new byte[8]), new PKCS7Padding());
  281. keyData = ssh2Сipher.Decrypt(reader.ReadBytes(blobSize));
  282. }
  283. else
  284. {
  285. throw new SshException(string.Format("Cipher method '{0}' is not supported.", cipherName));
  286. }
  287. /*
  288. * TODO: Create two specific data types to avoid using SshDataReader class.
  289. */
  290. reader = new SshDataReader(keyData);
  291. var decryptedLength = reader.ReadUInt32();
  292. if (decryptedLength > blobSize - 4)
  293. {
  294. throw new SshException("Invalid passphrase.");
  295. }
  296. if (keyType == "if-modn{sign{rsa-pkcs1-sha1},encrypt{rsa-pkcs1v2-oaep}}")
  297. {
  298. var exponent = reader.ReadBigIntWithBits(); // e
  299. var d = reader.ReadBigIntWithBits(); // d
  300. var modulus = reader.ReadBigIntWithBits(); // n
  301. var inverseQ = reader.ReadBigIntWithBits(); // u
  302. var q = reader.ReadBigIntWithBits(); // p
  303. var p = reader.ReadBigIntWithBits(); // q
  304. var decryptedRsaKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
  305. _key = decryptedRsaKey;
  306. #pragma warning disable CA2000 // Dispose objects before losing scope
  307. _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-512", _key, new RsaDigitalSignature(decryptedRsaKey, HashAlgorithmName.SHA512)));
  308. _hostAlgorithms.Add(new KeyHostAlgorithm("rsa-sha2-256", _key, new RsaDigitalSignature(decryptedRsaKey, HashAlgorithmName.SHA256)));
  309. #pragma warning restore CA2000 // Dispose objects before losing scope
  310. _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-rsa", _key));
  311. }
  312. else if (keyType == "dl-modp{sign{dsa-nist-sha1},dh{plain}}")
  313. {
  314. var zero = reader.ReadUInt32();
  315. if (zero != 0)
  316. {
  317. throw new SshException("Invalid private key");
  318. }
  319. var p = reader.ReadBigIntWithBits();
  320. var g = reader.ReadBigIntWithBits();
  321. var q = reader.ReadBigIntWithBits();
  322. var y = reader.ReadBigIntWithBits();
  323. var x = reader.ReadBigIntWithBits();
  324. _key = new DsaKey(p, q, g, y, x);
  325. _hostAlgorithms.Add(new KeyHostAlgorithm("ssh-dss", _key));
  326. }
  327. else
  328. {
  329. throw new NotSupportedException(string.Format("Key type '{0}' is not supported.", keyType));
  330. }
  331. break;
  332. default:
  333. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", keyName));
  334. }
  335. }
  336. private static byte[] GetCipherKey(string passphrase, int length)
  337. {
  338. var cipherKey = new List<byte>();
  339. using (var md5 = CryptoAbstraction.CreateMD5())
  340. {
  341. var passwordBytes = Encoding.UTF8.GetBytes(passphrase);
  342. var hash = md5.ComputeHash(passwordBytes);
  343. cipherKey.AddRange(hash);
  344. while (cipherKey.Count < length)
  345. {
  346. hash = passwordBytes.Concat(hash);
  347. hash = md5.ComputeHash(hash);
  348. cipherKey.AddRange(hash);
  349. }
  350. }
  351. return cipherKey.ToArray().Take(length);
  352. }
  353. /// <summary>
  354. /// Decrypts encrypted private key file data.
  355. /// </summary>
  356. /// <param name="cipherInfo">The cipher info.</param>
  357. /// <param name="cipherData">Encrypted data.</param>
  358. /// <param name="passPhrase">Decryption pass phrase.</param>
  359. /// <param name="binarySalt">Decryption binary salt.</param>
  360. /// <returns>Decrypted byte array.</returns>
  361. /// <exception cref="ArgumentNullException"><paramref name="cipherInfo" />, <paramref name="cipherData" />, <paramref name="passPhrase" /> or <paramref name="binarySalt" /> is <see langword="null"/>.</exception>
  362. private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt)
  363. {
  364. if (cipherInfo is null)
  365. {
  366. throw new ArgumentNullException(nameof(cipherInfo));
  367. }
  368. if (cipherData is null)
  369. {
  370. throw new ArgumentNullException(nameof(cipherData));
  371. }
  372. if (binarySalt is null)
  373. {
  374. throw new ArgumentNullException(nameof(binarySalt));
  375. }
  376. var cipherKey = new List<byte>();
  377. using (var md5 = CryptoAbstraction.CreateMD5())
  378. {
  379. var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
  380. // Use 8 bytes binary salt
  381. var initVector = passwordBytes.Concat(binarySalt.Take(8));
  382. var hash = md5.ComputeHash(initVector);
  383. cipherKey.AddRange(hash);
  384. while (cipherKey.Count < cipherInfo.KeySize / 8)
  385. {
  386. hash = hash.Concat(initVector);
  387. hash = md5.ComputeHash(hash);
  388. cipherKey.AddRange(hash);
  389. }
  390. }
  391. var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
  392. return cipher.Decrypt(cipherData);
  393. }
  394. /// <summary>
  395. /// Parses an OpenSSH V1 key file (i.e. ED25519 key) according to the the key spec:
  396. /// https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.key.
  397. /// </summary>
  398. /// <param name="keyFileData">The key file data (i.e. base64 encoded data between the header/footer).</param>
  399. /// <param name="passPhrase">Passphrase or <see langword="null"/> if there isn't one.</param>
  400. /// <returns>
  401. /// The OpenSSH V1 key.
  402. /// </returns>
  403. private static Key ParseOpenSshV1Key(byte[] keyFileData, string passPhrase)
  404. {
  405. var keyReader = new SshDataReader(keyFileData);
  406. // check magic header
  407. var authMagic = Encoding.UTF8.GetBytes("openssh-key-v1\0");
  408. var keyHeaderBytes = keyReader.ReadBytes(authMagic.Length);
  409. if (!authMagic.IsEqualTo(keyHeaderBytes))
  410. {
  411. throw new SshException("This openssh key does not contain the 'openssh-key-v1' format magic header");
  412. }
  413. // cipher will be "aes256-cbc" if using a passphrase, "none" otherwise
  414. var cipherName = keyReader.ReadString(Encoding.UTF8);
  415. // key derivation function (kdf): bcrypt or nothing
  416. var kdfName = keyReader.ReadString(Encoding.UTF8);
  417. // kdf options length: 24 if passphrase, 0 if no passphrase
  418. var kdfOptionsLen = (int)keyReader.ReadUInt32();
  419. byte[] salt = null;
  420. var rounds = 0;
  421. if (kdfOptionsLen > 0)
  422. {
  423. var saltLength = (int) keyReader.ReadUInt32();
  424. salt = keyReader.ReadBytes(saltLength);
  425. rounds = (int) keyReader.ReadUInt32();
  426. }
  427. // number of public keys, only supporting 1 for now
  428. var numberOfPublicKeys = (int)keyReader.ReadUInt32();
  429. if (numberOfPublicKeys != 1)
  430. {
  431. throw new SshException("At this time only one public key in the openssh key is supported.");
  432. }
  433. // read public key in ssh-format, but we dont need it
  434. _ = keyReader.ReadString(Encoding.UTF8);
  435. // possibly encrypted private key
  436. var privateKeyLength = (int) keyReader.ReadUInt32();
  437. var privateKeyBytes = keyReader.ReadBytes(privateKeyLength);
  438. // decrypt private key if necessary
  439. if (cipherName != "none")
  440. {
  441. if (string.IsNullOrEmpty(passPhrase))
  442. {
  443. throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
  444. }
  445. if (string.IsNullOrEmpty(kdfName) || kdfName != "bcrypt")
  446. {
  447. throw new SshException("kdf " + kdfName + " is not supported for openssh key file");
  448. }
  449. // inspired by the SSHj library (https://github.com/hierynomus/sshj)
  450. // apply the kdf to derive a key and iv from the passphrase
  451. var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
  452. var keyiv = new byte[48];
  453. new BCrypt().Pbkdf(passPhraseBytes, salt, rounds, keyiv);
  454. var key = new byte[32];
  455. Array.Copy(keyiv, 0, key, 0, 32);
  456. var iv = new byte[16];
  457. Array.Copy(keyiv, 32, iv, 0, 16);
  458. AesCipher cipher;
  459. switch (cipherName)
  460. {
  461. case "aes256-cbc":
  462. cipher = new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: false);
  463. break;
  464. case "aes256-ctr":
  465. cipher = new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false);
  466. break;
  467. default:
  468. throw new SshException("Cipher '" + cipherName + "' is not supported for an OpenSSH key.");
  469. }
  470. try
  471. {
  472. privateKeyBytes = cipher.Decrypt(privateKeyBytes);
  473. }
  474. finally
  475. {
  476. cipher.Dispose();
  477. }
  478. }
  479. // validate private key length
  480. privateKeyLength = privateKeyBytes.Length;
  481. if (privateKeyLength % 8 != 0)
  482. {
  483. throw new SshException("The private key section must be a multiple of the block size (8)");
  484. }
  485. // now parse the data we called the private key, it actually contains the public key again
  486. // so we need to parse through it to get the private key bytes, plus there's some
  487. // validation we need to do.
  488. var privateKeyReader = new SshDataReader(privateKeyBytes);
  489. // check ints should match, they wouldn't match for example if the wrong passphrase was supplied
  490. var checkInt1 = (int) privateKeyReader.ReadUInt32();
  491. var checkInt2 = (int) privateKeyReader.ReadUInt32();
  492. if (checkInt1 != checkInt2)
  493. {
  494. throw new SshException(string.Format(CultureInfo.InvariantCulture,
  495. "The random check bytes of the OpenSSH key do not match ({0} <-> {1}).",
  496. checkInt1.ToString(CultureInfo.InvariantCulture),
  497. checkInt2.ToString(CultureInfo.InvariantCulture)));
  498. }
  499. // key type
  500. var keyType = privateKeyReader.ReadString(Encoding.UTF8);
  501. Key parsedKey;
  502. byte[] publicKey;
  503. byte[] unencryptedPrivateKey;
  504. switch (keyType)
  505. {
  506. case "ssh-ed25519":
  507. // https://datatracker.ietf.org/doc/html/draft-miller-ssh-agent-11#section-3.2.3
  508. // ENC(A)
  509. _ = privateKeyReader.ReadBignum2();
  510. // k || ENC(A)
  511. unencryptedPrivateKey = privateKeyReader.ReadBignum2();
  512. parsedKey = new ED25519Key(unencryptedPrivateKey);
  513. break;
  514. case "ecdsa-sha2-nistp256":
  515. case "ecdsa-sha2-nistp384":
  516. case "ecdsa-sha2-nistp521":
  517. // curve
  518. var len = (int) privateKeyReader.ReadUInt32();
  519. var curve = Encoding.ASCII.GetString(privateKeyReader.ReadBytes(len));
  520. // public key
  521. publicKey = privateKeyReader.ReadBignum2();
  522. // private key
  523. unencryptedPrivateKey = privateKeyReader.ReadBignum2();
  524. parsedKey = new EcdsaKey(curve, publicKey, unencryptedPrivateKey.TrimLeadingZeros());
  525. break;
  526. case "ssh-rsa":
  527. var modulus = privateKeyReader.ReadBignum(); // n
  528. var exponent = privateKeyReader.ReadBignum(); // e
  529. var d = privateKeyReader.ReadBignum(); // d
  530. var inverseQ = privateKeyReader.ReadBignum(); // iqmp
  531. var p = privateKeyReader.ReadBignum(); // p
  532. var q = privateKeyReader.ReadBignum(); // q
  533. parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
  534. break;
  535. default:
  536. throw new SshException("OpenSSH key type '" + keyType + "' is not supported.");
  537. }
  538. parsedKey.Comment = privateKeyReader.ReadString(Encoding.UTF8);
  539. // The list of privatekey/comment pairs is padded with the bytes 1, 2, 3, ...
  540. // until the total length is a multiple of the cipher block size.
  541. var padding = privateKeyReader.ReadBytes();
  542. for (var i = 0; i < padding.Length; i++)
  543. {
  544. if ((int) padding[i] != i + 1)
  545. {
  546. throw new SshException("Padding of openssh key format contained wrong byte at position: " +
  547. i.ToString(CultureInfo.InvariantCulture));
  548. }
  549. }
  550. return parsedKey;
  551. }
  552. /// <summary>
  553. /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
  554. /// </summary>
  555. public void Dispose()
  556. {
  557. Dispose(disposing: true);
  558. GC.SuppressFinalize(this);
  559. }
  560. /// <summary>
  561. /// Releases unmanaged and - optionally - managed resources.
  562. /// </summary>
  563. /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
  564. protected virtual void Dispose(bool disposing)
  565. {
  566. if (_isDisposed)
  567. {
  568. return;
  569. }
  570. if (disposing)
  571. {
  572. var key = _key;
  573. if (key != null)
  574. {
  575. ((IDisposable) key).Dispose();
  576. _key = null;
  577. }
  578. _isDisposed = true;
  579. }
  580. }
  581. /// <summary>
  582. /// Finalizes an instance of the <see cref="PrivateKeyFile"/> class.
  583. /// </summary>
  584. ~PrivateKeyFile()
  585. {
  586. Dispose(disposing: false);
  587. }
  588. private sealed class SshDataReader : SshData
  589. {
  590. public SshDataReader(byte[] data)
  591. {
  592. Load(data);
  593. }
  594. public new uint ReadUInt32()
  595. {
  596. return base.ReadUInt32();
  597. }
  598. public new string ReadString(Encoding encoding)
  599. {
  600. return base.ReadString(encoding);
  601. }
  602. public new byte[] ReadBytes(int length)
  603. {
  604. return base.ReadBytes(length);
  605. }
  606. public new byte[] ReadBytes()
  607. {
  608. return base.ReadBytes();
  609. }
  610. /// <summary>
  611. /// Reads next mpint data type from internal buffer where length specified in bits.
  612. /// </summary>
  613. /// <returns>mpint read.</returns>
  614. public BigInteger ReadBigIntWithBits()
  615. {
  616. var length = (int) base.ReadUInt32();
  617. length = (length + 7) / 8;
  618. var data = base.ReadBytes(length);
  619. var bytesArray = new byte[data.Length + 1];
  620. Buffer.BlockCopy(data, 0, bytesArray, 1, data.Length);
  621. return new BigInteger(bytesArray.Reverse());
  622. }
  623. public BigInteger ReadBignum()
  624. {
  625. return new BigInteger(ReadBignum2().Reverse());
  626. }
  627. public byte[] ReadBignum2()
  628. {
  629. var length = (int)base.ReadUInt32();
  630. return base.ReadBytes(length);
  631. }
  632. protected override void LoadData()
  633. {
  634. }
  635. protected override void SaveData()
  636. {
  637. }
  638. }
  639. }
  640. }