ssh-private.js 6.7 KB

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