ssh-private.js 6.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. // Copyright 2015 Joyent, Inc.
  2. module.exports = {
  3. read: read,
  4. readSSHPrivate: readSSHPrivate,
  5. write: write
  6. };
  7. var assert = require('assert-plus');
  8. var asn1 = require('asn1');
  9. var Buffer = require('safer-buffer').Buffer;
  10. var algs = require('../algs');
  11. var utils = require('../utils');
  12. var crypto = require('crypto');
  13. var Key = require('../key');
  14. var PrivateKey = require('../private-key');
  15. var pem = require('./pem');
  16. var rfc4253 = require('./rfc4253');
  17. var SSHBuffer = require('../ssh-buffer');
  18. var errors = require('../errors');
  19. var bcrypt;
  20. function read(buf, options) {
  21. return (pem.read(buf, options));
  22. }
  23. var MAGIC = 'openssh-key-v1';
  24. function readSSHPrivate(type, buf, options) {
  25. buf = new SSHBuffer({buffer: buf});
  26. var magic = buf.readCString();
  27. assert.strictEqual(magic, MAGIC, 'bad magic string');
  28. var cipher = buf.readString();
  29. var kdf = buf.readString();
  30. var kdfOpts = buf.readBuffer();
  31. var nkeys = buf.readInt();
  32. if (nkeys !== 1) {
  33. throw (new Error('OpenSSH-format key file contains ' +
  34. 'multiple keys: this is unsupported.'));
  35. }
  36. var pubKey = buf.readBuffer();
  37. if (type === 'public') {
  38. assert.ok(buf.atEnd(), 'excess bytes left after key');
  39. return (rfc4253.read(pubKey));
  40. }
  41. var privKeyBlob = buf.readBuffer();
  42. assert.ok(buf.atEnd(), 'excess bytes left after key');
  43. var kdfOptsBuf = new SSHBuffer({ buffer: kdfOpts });
  44. switch (kdf) {
  45. case 'none':
  46. if (cipher !== 'none') {
  47. throw (new Error('OpenSSH-format key uses KDF "none" ' +
  48. 'but specifies a cipher other than "none"'));
  49. }
  50. break;
  51. case 'bcrypt':
  52. var salt = kdfOptsBuf.readBuffer();
  53. var rounds = kdfOptsBuf.readInt();
  54. var cinf = utils.opensshCipherInfo(cipher);
  55. if (bcrypt === undefined) {
  56. bcrypt = require('bcrypt-pbkdf');
  57. }
  58. if (typeof (options.passphrase) === 'string') {
  59. options.passphrase = Buffer.from(options.passphrase,
  60. 'utf-8');
  61. }
  62. if (!Buffer.isBuffer(options.passphrase)) {
  63. throw (new errors.KeyEncryptedError(
  64. options.filename, 'OpenSSH'));
  65. }
  66. var pass = new Uint8Array(options.passphrase);
  67. var salti = new Uint8Array(salt);
  68. /* Use the pbkdf to derive both the key and the IV. */
  69. var out = new Uint8Array(cinf.keySize + cinf.blockSize);
  70. var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length,
  71. out, out.length, rounds);
  72. if (res !== 0) {
  73. throw (new Error('bcrypt_pbkdf function returned ' +
  74. 'failure, parameters invalid'));
  75. }
  76. out = Buffer.from(out);
  77. var ckey = out.slice(0, cinf.keySize);
  78. var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize);
  79. var cipherStream = crypto.createDecipheriv(cinf.opensslName,
  80. ckey, iv);
  81. cipherStream.setAutoPadding(false);
  82. var chunk, chunks = [];
  83. cipherStream.once('error', function (e) {
  84. if (e.toString().indexOf('bad decrypt') !== -1) {
  85. throw (new Error('Incorrect passphrase ' +
  86. 'supplied, could not decrypt key'));
  87. }
  88. throw (e);
  89. });
  90. cipherStream.write(privKeyBlob);
  91. cipherStream.end();
  92. while ((chunk = cipherStream.read()) !== null)
  93. chunks.push(chunk);
  94. privKeyBlob = Buffer.concat(chunks);
  95. break;
  96. default:
  97. throw (new Error(
  98. 'OpenSSH-format key uses unknown KDF "' + kdf + '"'));
  99. }
  100. buf = new SSHBuffer({buffer: privKeyBlob});
  101. var checkInt1 = buf.readInt();
  102. var checkInt2 = buf.readInt();
  103. if (checkInt1 !== checkInt2) {
  104. throw (new Error('Incorrect passphrase supplied, could not ' +
  105. 'decrypt key'));
  106. }
  107. var ret = {};
  108. var key = rfc4253.readInternal(ret, 'private', buf.remainder());
  109. buf.skip(ret.consumed);
  110. var comment = buf.readString();
  111. key.comment = comment;
  112. return (key);
  113. }
  114. function write(key, options) {
  115. var pubKey;
  116. if (PrivateKey.isPrivateKey(key))
  117. pubKey = key.toPublic();
  118. else
  119. pubKey = key;
  120. var cipher = 'none';
  121. var kdf = 'none';
  122. var kdfopts = Buffer.alloc(0);
  123. var cinf = { blockSize: 8 };
  124. var passphrase;
  125. if (options !== undefined) {
  126. passphrase = options.passphrase;
  127. if (typeof (passphrase) === 'string')
  128. passphrase = Buffer.from(passphrase, 'utf-8');
  129. if (passphrase !== undefined) {
  130. assert.buffer(passphrase, 'options.passphrase');
  131. assert.optionalString(options.cipher, 'options.cipher');
  132. cipher = options.cipher;
  133. if (cipher === undefined)
  134. cipher = 'aes128-ctr';
  135. cinf = utils.opensshCipherInfo(cipher);
  136. kdf = 'bcrypt';
  137. }
  138. }
  139. var privBuf;
  140. if (PrivateKey.isPrivateKey(key)) {
  141. privBuf = new SSHBuffer({});
  142. var checkInt = crypto.randomBytes(4).readUInt32BE(0);
  143. privBuf.writeInt(checkInt);
  144. privBuf.writeInt(checkInt);
  145. privBuf.write(key.toBuffer('rfc4253'));
  146. privBuf.writeString(key.comment || '');
  147. var n = 1;
  148. while (privBuf._offset % cinf.blockSize !== 0)
  149. privBuf.writeChar(n++);
  150. privBuf = privBuf.toBuffer();
  151. }
  152. switch (kdf) {
  153. case 'none':
  154. break;
  155. case 'bcrypt':
  156. var salt = crypto.randomBytes(16);
  157. var rounds = 16;
  158. var kdfssh = new SSHBuffer({});
  159. kdfssh.writeBuffer(salt);
  160. kdfssh.writeInt(rounds);
  161. kdfopts = kdfssh.toBuffer();
  162. if (bcrypt === undefined) {
  163. bcrypt = require('bcrypt-pbkdf');
  164. }
  165. var pass = new Uint8Array(passphrase);
  166. var salti = new Uint8Array(salt);
  167. /* Use the pbkdf to derive both the key and the IV. */
  168. var out = new Uint8Array(cinf.keySize + cinf.blockSize);
  169. var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length,
  170. out, out.length, rounds);
  171. if (res !== 0) {
  172. throw (new Error('bcrypt_pbkdf function returned ' +
  173. 'failure, parameters invalid'));
  174. }
  175. out = Buffer.from(out);
  176. var ckey = out.slice(0, cinf.keySize);
  177. var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize);
  178. var cipherStream = crypto.createCipheriv(cinf.opensslName,
  179. ckey, iv);
  180. cipherStream.setAutoPadding(false);
  181. var chunk, chunks = [];
  182. cipherStream.once('error', function (e) {
  183. throw (e);
  184. });
  185. cipherStream.write(privBuf);
  186. cipherStream.end();
  187. while ((chunk = cipherStream.read()) !== null)
  188. chunks.push(chunk);
  189. privBuf = Buffer.concat(chunks);
  190. break;
  191. default:
  192. throw (new Error('Unsupported kdf ' + kdf));
  193. }
  194. var buf = new SSHBuffer({});
  195. buf.writeCString(MAGIC);
  196. buf.writeString(cipher); /* cipher */
  197. buf.writeString(kdf); /* kdf */
  198. buf.writeBuffer(kdfopts); /* kdfoptions */
  199. buf.writeInt(1); /* nkeys */
  200. buf.writeBuffer(pubKey.toBuffer('rfc4253'));
  201. if (privBuf)
  202. buf.writeBuffer(privBuf);
  203. buf = buf.toBuffer();
  204. var header;
  205. if (PrivateKey.isPrivateKey(key))
  206. header = 'OPENSSH PRIVATE KEY';
  207. else
  208. header = 'OPENSSH PUBLIC KEY';
  209. var tmp = buf.toString('base64');
  210. var len = tmp.length + (tmp.length / 70) +
  211. 18 + 16 + header.length*2 + 10;
  212. buf = Buffer.alloc(len);
  213. var o = 0;
  214. o += buf.write('-----BEGIN ' + header + '-----\n', o);
  215. for (var i = 0; i < tmp.length; ) {
  216. var limit = i + 70;
  217. if (limit > tmp.length)
  218. limit = tmp.length;
  219. o += buf.write(tmp.slice(i, limit), o);
  220. buf[o++] = 10;
  221. i = limit;
  222. }
  223. o += buf.write('-----END ' + header + '-----\n', o);
  224. return (buf.slice(0, o));
  225. }