PrivateKeyFile.PuTTY.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Linq;
  6. using System.Security.Cryptography;
  7. using System.Text;
  8. using Org.BouncyCastle.Crypto.Generators;
  9. using Org.BouncyCastle.Crypto.Parameters;
  10. using Renci.SshNet.Abstractions;
  11. using Renci.SshNet.Common;
  12. using Renci.SshNet.Security;
  13. using Renci.SshNet.Security.Cryptography.Ciphers;
  14. namespace Renci.SshNet
  15. {
  16. public partial class PrivateKeyFile
  17. {
  18. private sealed class PuTTY : IPrivateKeyParser
  19. {
  20. private readonly string _version;
  21. private readonly string _algorithmName;
  22. private readonly string _encryptionType;
  23. private readonly string _comment;
  24. private readonly byte[] _publicKey;
  25. private readonly string? _argon2Type;
  26. private readonly string? _argon2Salt;
  27. private readonly string? _argon2Iterations;
  28. private readonly string? _argon2Memory;
  29. private readonly string? _argon2Parallelism;
  30. private readonly byte[] _data;
  31. private readonly string _mac;
  32. private readonly string? _passPhrase;
  33. public PuTTY(string version, string algorithmName, string encryptionType, string comment, byte[] publicKey, string? argon2Type, string? argon2Salt, string? argon2Iterations, string? argon2Memory, string? argon2Parallelism, byte[] data, string mac, string? passPhrase)
  34. {
  35. _version = version;
  36. _algorithmName = algorithmName;
  37. _encryptionType = encryptionType;
  38. _comment = comment;
  39. _publicKey = publicKey;
  40. _argon2Type = argon2Type;
  41. _argon2Salt = argon2Salt;
  42. _argon2Iterations = argon2Iterations;
  43. _argon2Memory = argon2Memory;
  44. _argon2Parallelism = argon2Parallelism;
  45. _data = data;
  46. _mac = mac;
  47. _passPhrase = passPhrase;
  48. }
  49. /// <summary>
  50. /// Parses an PuTTY PPK key file.
  51. /// <see href="https://tartarus.org/~simon/putty-snapshots/htmldoc/AppendixC.html"/>.
  52. /// </summary>
  53. public Key Parse()
  54. {
  55. byte[] privateKey;
  56. HMAC hmac;
  57. switch (_encryptionType)
  58. {
  59. case "aes256-cbc":
  60. if (string.IsNullOrEmpty(_passPhrase))
  61. {
  62. throw new SshPassPhraseNullOrEmptyException("Private key is encrypted but passphrase is empty.");
  63. }
  64. byte[] cipherKey;
  65. byte[] cipherIV;
  66. switch (_version)
  67. {
  68. case "3":
  69. ThrowHelper.ThrowIfNullOrEmpty(_argon2Type);
  70. ThrowHelper.ThrowIfNullOrEmpty(_argon2Iterations);
  71. ThrowHelper.ThrowIfNullOrEmpty(_argon2Memory);
  72. ThrowHelper.ThrowIfNullOrEmpty(_argon2Parallelism);
  73. ThrowHelper.ThrowIfNullOrEmpty(_argon2Salt);
  74. var keyData = Argon2(
  75. _argon2Type,
  76. Convert.ToInt32(_argon2Iterations),
  77. Convert.ToInt32(_argon2Memory),
  78. Convert.ToInt32(_argon2Parallelism),
  79. #if NET
  80. Convert.FromHexString(_argon2Salt),
  81. #else
  82. Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_argon2Salt),
  83. #endif
  84. _passPhrase);
  85. cipherKey = keyData.Take(32);
  86. cipherIV = keyData.Take(32, 16);
  87. var macKey = keyData.Take(48, 32);
  88. hmac = new HMACSHA256(macKey);
  89. break;
  90. case "2":
  91. keyData = V2KDF(_passPhrase);
  92. cipherKey = keyData.Take(32);
  93. cipherIV = new byte[16];
  94. macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key" + _passPhrase)).Take(20);
  95. hmac = new HMACSHA1(macKey);
  96. break;
  97. default:
  98. throw new SshException("PuTTY key file version " + _version + " is not supported");
  99. }
  100. using (var cipher = new AesCipher(cipherKey, cipherIV, AesCipherMode.CBC, pkcs7Padding: false))
  101. {
  102. privateKey = cipher.Decrypt(_data);
  103. }
  104. break;
  105. case "none":
  106. switch (_version)
  107. {
  108. case "3":
  109. hmac = new HMACSHA256(Array.Empty<byte>());
  110. break;
  111. case "2":
  112. var macKey = CryptoAbstraction.HashSHA1(Encoding.UTF8.GetBytes("putty-private-key-file-mac-key"));
  113. hmac = new HMACSHA1(macKey);
  114. break;
  115. default:
  116. throw new SshException("PuTTY key file version " + _version + " is not supported");
  117. }
  118. privateKey = _data;
  119. break;
  120. default:
  121. throw new SshException("Encryption " + _encryptionType + " is not supported for PuTTY key file");
  122. }
  123. byte[] macData;
  124. using (var macStream = new SshDataStream(256))
  125. {
  126. macStream.Write(_algorithmName, Encoding.UTF8);
  127. macStream.Write(_encryptionType, Encoding.UTF8);
  128. macStream.Write(_comment, Encoding.UTF8);
  129. macStream.WriteBinary(_publicKey);
  130. macStream.WriteBinary(privateKey);
  131. macData = macStream.ToArray();
  132. }
  133. byte[] macValue;
  134. using (hmac)
  135. {
  136. macValue = hmac.ComputeHash(macData);
  137. }
  138. #if NET
  139. var reference = Convert.FromHexString(_mac);
  140. #else
  141. var reference = Org.BouncyCastle.Utilities.Encoders.Hex.Decode(_mac);
  142. #endif
  143. if (!macValue.SequenceEqual(reference))
  144. {
  145. throw new SshException("MAC verification failed for PuTTY key file");
  146. }
  147. var publicKeyReader = new SshDataReader(_publicKey);
  148. var keyType = publicKeyReader.ReadString(Encoding.UTF8);
  149. Debug.Assert(keyType == _algorithmName, $"{nameof(keyType)} is not the same as {nameof(_algorithmName)}");
  150. var privateKeyReader = new SshDataReader(privateKey);
  151. Key parsedKey;
  152. switch (keyType)
  153. {
  154. case "ssh-ed25519":
  155. parsedKey = new ED25519Key(privateKeyReader.ReadBignum2());
  156. break;
  157. case "ecdsa-sha2-nistp256":
  158. case "ecdsa-sha2-nistp384":
  159. case "ecdsa-sha2-nistp521":
  160. var curve = publicKeyReader.ReadString(Encoding.ASCII);
  161. var pub = publicKeyReader.ReadBignum2();
  162. var prv = privateKeyReader.ReadBignum2();
  163. parsedKey = new EcdsaKey(curve, pub, prv);
  164. break;
  165. case "ssh-rsa":
  166. var exponent = publicKeyReader.ReadBignum(); // e
  167. var modulus = publicKeyReader.ReadBignum(); // n
  168. var d = privateKeyReader.ReadBignum(); // d
  169. var p = privateKeyReader.ReadBignum(); // p
  170. var q = privateKeyReader.ReadBignum(); // q
  171. var inverseQ = privateKeyReader.ReadBignum(); // iqmp
  172. parsedKey = new RsaKey(modulus, exponent, d, p, q, inverseQ);
  173. break;
  174. default:
  175. throw new SshException("Key type " + keyType + " is not supported for PuTTY key file");
  176. }
  177. parsedKey.Comment = _comment;
  178. return parsedKey;
  179. }
  180. private static byte[] Argon2(string type, int iterations, int memory, int parallelism, byte[] salt, string passPhrase)
  181. {
  182. int param;
  183. switch (type)
  184. {
  185. case "Argon2i":
  186. param = Argon2Parameters.Argon2i;
  187. break;
  188. case "Argon2d":
  189. param = Argon2Parameters.Argon2d;
  190. break;
  191. case "Argon2id":
  192. param = Argon2Parameters.Argon2id;
  193. break;
  194. default:
  195. throw new SshException("KDF " + type + " is not supported for PuTTY key file");
  196. }
  197. var a2p = new Argon2Parameters.Builder(param)
  198. .WithVersion(Argon2Parameters.Version13)
  199. .WithIterations(iterations)
  200. .WithMemoryAsKB(memory)
  201. .WithParallelism(parallelism)
  202. .WithSalt(salt).Build();
  203. var generator = new Argon2BytesGenerator();
  204. generator.Init(a2p);
  205. var output = new byte[80];
  206. var bytes = generator.GenerateBytes(passPhrase.ToCharArray(), output);
  207. if (bytes != output.Length)
  208. {
  209. throw new SshException("Failed to generate key via Argon2");
  210. }
  211. return output;
  212. }
  213. private static byte[] V2KDF(string passPhrase)
  214. {
  215. var cipherKey = new List<byte>();
  216. var passPhraseBytes = Encoding.UTF8.GetBytes(passPhrase);
  217. for (var sequenceNumber = 0; sequenceNumber < 2; sequenceNumber++)
  218. {
  219. using (var sha1 = SHA1.Create())
  220. {
  221. var sequence = new byte[] { 0, 0, 0, (byte)sequenceNumber };
  222. _ = sha1.TransformBlock(sequence, 0, 4, outputBuffer: null, 0);
  223. _ = sha1.TransformFinalBlock(passPhraseBytes, 0, passPhraseBytes.Length);
  224. Debug.Assert(sha1.Hash != null, "Hash is null");
  225. cipherKey.AddRange(sha1.Hash);
  226. }
  227. }
  228. return cipherKey.ToArray();
  229. }
  230. }
  231. }
  232. }