PrivateKeyFile.PKCS1.cs 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Globalization;
  6. using System.Security.Cryptography;
  7. using System.Text;
  8. using Renci.SshNet.Common;
  9. using Renci.SshNet.Security;
  10. using Renci.SshNet.Security.Cryptography.Ciphers;
  11. using CipherMode = System.Security.Cryptography.CipherMode;
  12. namespace Renci.SshNet
  13. {
  14. public partial class PrivateKeyFile
  15. {
  16. private sealed class PKCS1 : IPrivateKeyParser
  17. {
  18. private readonly string _cipherName;
  19. private readonly string _salt;
  20. private readonly string _keyName;
  21. private readonly byte[] _data;
  22. private readonly string? _passPhrase;
  23. public PKCS1(string cipherName, string salt, string keyName, byte[] data, string? passPhrase)
  24. {
  25. _cipherName = cipherName;
  26. _salt = salt;
  27. _keyName = keyName;
  28. _data = data;
  29. _passPhrase = passPhrase;
  30. }
  31. public Key Parse()
  32. {
  33. byte[] decryptedData;
  34. if (!string.IsNullOrEmpty(_cipherName) && !string.IsNullOrEmpty(_salt))
  35. {
  36. if (string.IsNullOrEmpty(_passPhrase))
  37. {
  38. throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
  39. }
  40. #if NET
  41. var binarySalt = Convert.FromHexString(_salt);
  42. #else
  43. var binarySalt = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_salt);
  44. #endif
  45. CipherInfo cipher;
  46. switch (_cipherName)
  47. {
  48. case "DES-EDE3-CBC":
  49. cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, iv, CipherMode.CBC, pkcs7Padding: true));
  50. break;
  51. case "DES-EDE3-CFB":
  52. cipher = new CipherInfo(192, (key, iv) => new TripleDesCipher(key, iv, CipherMode.CFB, pkcs7Padding: false));
  53. break;
  54. case "AES-128-CBC":
  55. cipher = new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
  56. break;
  57. case "AES-192-CBC":
  58. cipher = new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
  59. break;
  60. case "AES-256-CBC":
  61. cipher = new CipherInfo(256, (key, iv) => new AesCipher(key, iv, AesCipherMode.CBC, pkcs7Padding: true));
  62. break;
  63. default:
  64. throw new SshException(string.Format(CultureInfo.InvariantCulture, "Private key cipher \"{0}\" is not supported.", _cipherName));
  65. }
  66. decryptedData = DecryptKey(cipher, _data, _passPhrase, binarySalt);
  67. }
  68. else
  69. {
  70. decryptedData = _data;
  71. }
  72. switch (_keyName)
  73. {
  74. case "RSA PRIVATE KEY":
  75. return new RsaKey(decryptedData);
  76. case "DSA PRIVATE KEY":
  77. return new DsaKey(decryptedData);
  78. case "EC PRIVATE KEY":
  79. return new EcdsaKey(decryptedData);
  80. default:
  81. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Key '{0}' is not supported.", _keyName));
  82. }
  83. }
  84. /// <summary>
  85. /// Decrypts encrypted private key file data.
  86. /// </summary>
  87. /// <param name="cipherInfo">The cipher info.</param>
  88. /// <param name="cipherData">Encrypted data.</param>
  89. /// <param name="passPhrase">Decryption pass phrase.</param>
  90. /// <param name="binarySalt">Decryption binary salt.</param>
  91. /// <returns>Decrypted byte array.</returns>
  92. /// <exception cref="ArgumentNullException"><paramref name="cipherInfo" />, <paramref name="cipherData" />, <paramref name="passPhrase" /> or <paramref name="binarySalt" /> is <see langword="null"/>.</exception>
  93. private static byte[] DecryptKey(CipherInfo cipherInfo, byte[] cipherData, string passPhrase, byte[] binarySalt)
  94. {
  95. Debug.Assert(cipherInfo != null);
  96. Debug.Assert(cipherData != null);
  97. Debug.Assert(binarySalt != null);
  98. var cipherKey = new List<byte>();
  99. #pragma warning disable CA1850 // Prefer static HashData method; We'll reuse the object on lower targets.
  100. using (var md5 = MD5.Create())
  101. {
  102. var passwordBytes = Encoding.UTF8.GetBytes(passPhrase);
  103. // Use 8 bytes binary salt
  104. var initVector = passwordBytes.Concat(binarySalt.Take(8));
  105. var hash = md5.ComputeHash(initVector);
  106. cipherKey.AddRange(hash);
  107. while (cipherKey.Count < cipherInfo.KeySize / 8)
  108. {
  109. hash = hash.Concat(initVector);
  110. hash = md5.ComputeHash(hash);
  111. cipherKey.AddRange(hash);
  112. }
  113. }
  114. #pragma warning restore CA1850 // Prefer static HashData method
  115. var cipher = cipherInfo.Cipher(cipherKey.ToArray(), binarySalt);
  116. try
  117. {
  118. return cipher.Decrypt(cipherData);
  119. }
  120. finally
  121. {
  122. if (cipher is IDisposable disposable)
  123. {
  124. disposable.Dispose();
  125. }
  126. }
  127. }
  128. }
  129. }
  130. }