You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

236 lines
7.0 KiB

4 years ago
  1. /**
  2. * Functions to output keys in SSH-friendly formats.
  3. *
  4. * This is part of the Forge project which may be used under the terms of
  5. * either the BSD License or the GNU General Public License (GPL) Version 2.
  6. *
  7. * See: https://github.com/digitalbazaar/forge/blob/cbebca3780658703d925b61b2caffb1d263a6c1d/LICENSE
  8. *
  9. * @author https://github.com/shellac
  10. */
  11. var forge = require('./forge');
  12. require('./aes');
  13. require('./hmac');
  14. require('./md5');
  15. require('./sha1');
  16. require('./util');
  17. var ssh = module.exports = forge.ssh = forge.ssh || {};
  18. /**
  19. * Encodes (and optionally encrypts) a private RSA key as a Putty PPK file.
  20. *
  21. * @param privateKey the key.
  22. * @param passphrase a passphrase to protect the key (falsy for no encryption).
  23. * @param comment a comment to include in the key file.
  24. *
  25. * @return the PPK file as a string.
  26. */
  27. ssh.privateKeyToPutty = function(privateKey, passphrase, comment) {
  28. comment = comment || '';
  29. passphrase = passphrase || '';
  30. var algorithm = 'ssh-rsa';
  31. var encryptionAlgorithm = (passphrase === '') ? 'none' : 'aes256-cbc';
  32. var ppk = 'PuTTY-User-Key-File-2: ' + algorithm + '\r\n';
  33. ppk += 'Encryption: ' + encryptionAlgorithm + '\r\n';
  34. ppk += 'Comment: ' + comment + '\r\n';
  35. // public key into buffer for ppk
  36. var pubbuffer = forge.util.createBuffer();
  37. _addStringToBuffer(pubbuffer, algorithm);
  38. _addBigIntegerToBuffer(pubbuffer, privateKey.e);
  39. _addBigIntegerToBuffer(pubbuffer, privateKey.n);
  40. // write public key
  41. var pub = forge.util.encode64(pubbuffer.bytes(), 64);
  42. var length = Math.floor(pub.length / 66) + 1; // 66 = 64 + \r\n
  43. ppk += 'Public-Lines: ' + length + '\r\n';
  44. ppk += pub;
  45. // private key into a buffer
  46. var privbuffer = forge.util.createBuffer();
  47. _addBigIntegerToBuffer(privbuffer, privateKey.d);
  48. _addBigIntegerToBuffer(privbuffer, privateKey.p);
  49. _addBigIntegerToBuffer(privbuffer, privateKey.q);
  50. _addBigIntegerToBuffer(privbuffer, privateKey.qInv);
  51. // optionally encrypt the private key
  52. var priv;
  53. if(!passphrase) {
  54. // use the unencrypted buffer
  55. priv = forge.util.encode64(privbuffer.bytes(), 64);
  56. } else {
  57. // encrypt RSA key using passphrase
  58. var encLen = privbuffer.length() + 16 - 1;
  59. encLen -= encLen % 16;
  60. // pad private key with sha1-d data -- needs to be a multiple of 16
  61. var padding = _sha1(privbuffer.bytes());
  62. padding.truncate(padding.length() - encLen + privbuffer.length());
  63. privbuffer.putBuffer(padding);
  64. var aeskey = forge.util.createBuffer();
  65. aeskey.putBuffer(_sha1('\x00\x00\x00\x00', passphrase));
  66. aeskey.putBuffer(_sha1('\x00\x00\x00\x01', passphrase));
  67. // encrypt some bytes using CBC mode
  68. // key is 40 bytes, so truncate *by* 8 bytes
  69. var cipher = forge.aes.createEncryptionCipher(aeskey.truncate(8), 'CBC');
  70. cipher.start(forge.util.createBuffer().fillWithByte(0, 16));
  71. cipher.update(privbuffer.copy());
  72. cipher.finish();
  73. var encrypted = cipher.output;
  74. // Note: this appears to differ from Putty -- is forge wrong, or putty?
  75. // due to padding we finish as an exact multiple of 16
  76. encrypted.truncate(16); // all padding
  77. priv = forge.util.encode64(encrypted.bytes(), 64);
  78. }
  79. // output private key
  80. length = Math.floor(priv.length / 66) + 1; // 64 + \r\n
  81. ppk += '\r\nPrivate-Lines: ' + length + '\r\n';
  82. ppk += priv;
  83. // MAC
  84. var mackey = _sha1('putty-private-key-file-mac-key', passphrase);
  85. var macbuffer = forge.util.createBuffer();
  86. _addStringToBuffer(macbuffer, algorithm);
  87. _addStringToBuffer(macbuffer, encryptionAlgorithm);
  88. _addStringToBuffer(macbuffer, comment);
  89. macbuffer.putInt32(pubbuffer.length());
  90. macbuffer.putBuffer(pubbuffer);
  91. macbuffer.putInt32(privbuffer.length());
  92. macbuffer.putBuffer(privbuffer);
  93. var hmac = forge.hmac.create();
  94. hmac.start('sha1', mackey);
  95. hmac.update(macbuffer.bytes());
  96. ppk += '\r\nPrivate-MAC: ' + hmac.digest().toHex() + '\r\n';
  97. return ppk;
  98. };
  99. /**
  100. * Encodes a public RSA key as an OpenSSH file.
  101. *
  102. * @param key the key.
  103. * @param comment a comment.
  104. *
  105. * @return the public key in OpenSSH format.
  106. */
  107. ssh.publicKeyToOpenSSH = function(key, comment) {
  108. var type = 'ssh-rsa';
  109. comment = comment || '';
  110. var buffer = forge.util.createBuffer();
  111. _addStringToBuffer(buffer, type);
  112. _addBigIntegerToBuffer(buffer, key.e);
  113. _addBigIntegerToBuffer(buffer, key.n);
  114. return type + ' ' + forge.util.encode64(buffer.bytes()) + ' ' + comment;
  115. };
  116. /**
  117. * Encodes a private RSA key as an OpenSSH file.
  118. *
  119. * @param key the key.
  120. * @param passphrase a passphrase to protect the key (falsy for no encryption).
  121. *
  122. * @return the public key in OpenSSH format.
  123. */
  124. ssh.privateKeyToOpenSSH = function(privateKey, passphrase) {
  125. if(!passphrase) {
  126. return forge.pki.privateKeyToPem(privateKey);
  127. }
  128. // OpenSSH private key is just a legacy format, it seems
  129. return forge.pki.encryptRsaPrivateKey(privateKey, passphrase,
  130. {legacy: true, algorithm: 'aes128'});
  131. };
  132. /**
  133. * Gets the SSH fingerprint for the given public key.
  134. *
  135. * @param options the options to use.
  136. * [md] the message digest object to use (defaults to forge.md.md5).
  137. * [encoding] an alternative output encoding, such as 'hex'
  138. * (defaults to none, outputs a byte buffer).
  139. * [delimiter] the delimiter to use between bytes for 'hex' encoded
  140. * output, eg: ':' (defaults to none).
  141. *
  142. * @return the fingerprint as a byte buffer or other encoding based on options.
  143. */
  144. ssh.getPublicKeyFingerprint = function(key, options) {
  145. options = options || {};
  146. var md = options.md || forge.md.md5.create();
  147. var type = 'ssh-rsa';
  148. var buffer = forge.util.createBuffer();
  149. _addStringToBuffer(buffer, type);
  150. _addBigIntegerToBuffer(buffer, key.e);
  151. _addBigIntegerToBuffer(buffer, key.n);
  152. // hash public key bytes
  153. md.start();
  154. md.update(buffer.getBytes());
  155. var digest = md.digest();
  156. if(options.encoding === 'hex') {
  157. var hex = digest.toHex();
  158. if(options.delimiter) {
  159. return hex.match(/.{2}/g).join(options.delimiter);
  160. }
  161. return hex;
  162. } else if(options.encoding === 'binary') {
  163. return digest.getBytes();
  164. } else if(options.encoding) {
  165. throw new Error('Unknown encoding "' + options.encoding + '".');
  166. }
  167. return digest;
  168. };
  169. /**
  170. * Adds len(val) then val to a buffer.
  171. *
  172. * @param buffer the buffer to add to.
  173. * @param val a big integer.
  174. */
  175. function _addBigIntegerToBuffer(buffer, val) {
  176. var hexVal = val.toString(16);
  177. // ensure 2s complement +ve
  178. if(hexVal[0] >= '8') {
  179. hexVal = '00' + hexVal;
  180. }
  181. var bytes = forge.util.hexToBytes(hexVal);
  182. buffer.putInt32(bytes.length);
  183. buffer.putBytes(bytes);
  184. }
  185. /**
  186. * Adds len(val) then val to a buffer.
  187. *
  188. * @param buffer the buffer to add to.
  189. * @param val a string.
  190. */
  191. function _addStringToBuffer(buffer, val) {
  192. buffer.putInt32(val.length);
  193. buffer.putString(val);
  194. }
  195. /**
  196. * Hashes the arguments into one value using SHA-1.
  197. *
  198. * @return the sha1 hash of the provided arguments.
  199. */
  200. function _sha1() {
  201. var sha = forge.md.sha1.create();
  202. var num = arguments.length;
  203. for (var i = 0; i < num; ++i) {
  204. sha.update(arguments[i]);
  205. }
  206. return sha.digest();
  207. }