| // Copyright 2015 Joyent, Inc. |
| |
| module.exports = { |
| read: read, |
| readSSHPrivate: readSSHPrivate, |
| write: write |
| }; |
| |
| var assert = require('assert-plus'); |
| var asn1 = require('asn1'); |
| var Buffer = require('safer-buffer').Buffer; |
| var algs = require('../algs'); |
| var utils = require('../utils'); |
| var crypto = require('crypto'); |
| |
| var Key = require('../key'); |
| var PrivateKey = require('../private-key'); |
| var pem = require('./pem'); |
| var rfc4253 = require('./rfc4253'); |
| var SSHBuffer = require('../ssh-buffer'); |
| var errors = require('../errors'); |
| |
| var bcrypt; |
| |
| function read(buf, options) { |
| return (pem.read(buf, options)); |
| } |
| |
| var MAGIC = 'openssh-key-v1'; |
| |
| function readSSHPrivate(type, buf, options) { |
| buf = new SSHBuffer({buffer: buf}); |
| |
| var magic = buf.readCString(); |
| assert.strictEqual(magic, MAGIC, 'bad magic string'); |
| |
| var cipher = buf.readString(); |
| var kdf = buf.readString(); |
| var kdfOpts = buf.readBuffer(); |
| |
| var nkeys = buf.readInt(); |
| if (nkeys !== 1) { |
| throw (new Error('OpenSSH-format key file contains ' + |
| 'multiple keys: this is unsupported.')); |
| } |
| |
| var pubKey = buf.readBuffer(); |
| |
| if (type === 'public') { |
| assert.ok(buf.atEnd(), 'excess bytes left after key'); |
| return (rfc4253.read(pubKey)); |
| } |
| |
| var privKeyBlob = buf.readBuffer(); |
| assert.ok(buf.atEnd(), 'excess bytes left after key'); |
| |
| var kdfOptsBuf = new SSHBuffer({ buffer: kdfOpts }); |
| switch (kdf) { |
| case 'none': |
| if (cipher !== 'none') { |
| throw (new Error('OpenSSH-format key uses KDF "none" ' + |
| 'but specifies a cipher other than "none"')); |
| } |
| break; |
| case 'bcrypt': |
| var salt = kdfOptsBuf.readBuffer(); |
| var rounds = kdfOptsBuf.readInt(); |
| var cinf = utils.opensshCipherInfo(cipher); |
| if (bcrypt === undefined) { |
| bcrypt = require('bcrypt-pbkdf'); |
| } |
| |
| if (typeof (options.passphrase) === 'string') { |
| options.passphrase = Buffer.from(options.passphrase, |
| 'utf-8'); |
| } |
| if (!Buffer.isBuffer(options.passphrase)) { |
| throw (new errors.KeyEncryptedError( |
| options.filename, 'OpenSSH')); |
| } |
| |
| var pass = new Uint8Array(options.passphrase); |
| var salti = new Uint8Array(salt); |
| /* Use the pbkdf to derive both the key and the IV. */ |
| var out = new Uint8Array(cinf.keySize + cinf.blockSize); |
| var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length, |
| out, out.length, rounds); |
| if (res !== 0) { |
| throw (new Error('bcrypt_pbkdf function returned ' + |
| 'failure, parameters invalid')); |
| } |
| out = Buffer.from(out); |
| var ckey = out.slice(0, cinf.keySize); |
| var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize); |
| var cipherStream = crypto.createDecipheriv(cinf.opensslName, |
| ckey, iv); |
| cipherStream.setAutoPadding(false); |
| var chunk, chunks = []; |
| cipherStream.once('error', function (e) { |
| if (e.toString().indexOf('bad decrypt') !== -1) { |
| throw (new Error('Incorrect passphrase ' + |
| 'supplied, could not decrypt key')); |
| } |
| throw (e); |
| }); |
| cipherStream.write(privKeyBlob); |
| cipherStream.end(); |
| while ((chunk = cipherStream.read()) !== null) |
| chunks.push(chunk); |
| privKeyBlob = Buffer.concat(chunks); |
| break; |
| default: |
| throw (new Error( |
| 'OpenSSH-format key uses unknown KDF "' + kdf + '"')); |
| } |
| |
| buf = new SSHBuffer({buffer: privKeyBlob}); |
| |
| var checkInt1 = buf.readInt(); |
| var checkInt2 = buf.readInt(); |
| if (checkInt1 !== checkInt2) { |
| throw (new Error('Incorrect passphrase supplied, could not ' + |
| 'decrypt key')); |
| } |
| |
| var ret = {}; |
| var key = rfc4253.readInternal(ret, 'private', buf.remainder()); |
| |
| buf.skip(ret.consumed); |
| |
| var comment = buf.readString(); |
| key.comment = comment; |
| |
| return (key); |
| } |
| |
| function write(key, options) { |
| var pubKey; |
| if (PrivateKey.isPrivateKey(key)) |
| pubKey = key.toPublic(); |
| else |
| pubKey = key; |
| |
| var cipher = 'none'; |
| var kdf = 'none'; |
| var kdfopts = Buffer.alloc(0); |
| var cinf = { blockSize: 8 }; |
| var passphrase; |
| if (options !== undefined) { |
| passphrase = options.passphrase; |
| if (typeof (passphrase) === 'string') |
| passphrase = Buffer.from(passphrase, 'utf-8'); |
| if (passphrase !== undefined) { |
| assert.buffer(passphrase, 'options.passphrase'); |
| assert.optionalString(options.cipher, 'options.cipher'); |
| cipher = options.cipher; |
| if (cipher === undefined) |
| cipher = 'aes128-ctr'; |
| cinf = utils.opensshCipherInfo(cipher); |
| kdf = 'bcrypt'; |
| } |
| } |
| |
| var privBuf; |
| if (PrivateKey.isPrivateKey(key)) { |
| privBuf = new SSHBuffer({}); |
| var checkInt = crypto.randomBytes(4).readUInt32BE(0); |
| privBuf.writeInt(checkInt); |
| privBuf.writeInt(checkInt); |
| privBuf.write(key.toBuffer('rfc4253')); |
| privBuf.writeString(key.comment || ''); |
| |
| var n = 1; |
| while (privBuf._offset % cinf.blockSize !== 0) |
| privBuf.writeChar(n++); |
| privBuf = privBuf.toBuffer(); |
| } |
| |
| switch (kdf) { |
| case 'none': |
| break; |
| case 'bcrypt': |
| var salt = crypto.randomBytes(16); |
| var rounds = 16; |
| var kdfssh = new SSHBuffer({}); |
| kdfssh.writeBuffer(salt); |
| kdfssh.writeInt(rounds); |
| kdfopts = kdfssh.toBuffer(); |
| |
| if (bcrypt === undefined) { |
| bcrypt = require('bcrypt-pbkdf'); |
| } |
| var pass = new Uint8Array(passphrase); |
| var salti = new Uint8Array(salt); |
| /* Use the pbkdf to derive both the key and the IV. */ |
| var out = new Uint8Array(cinf.keySize + cinf.blockSize); |
| var res = bcrypt.pbkdf(pass, pass.length, salti, salti.length, |
| out, out.length, rounds); |
| if (res !== 0) { |
| throw (new Error('bcrypt_pbkdf function returned ' + |
| 'failure, parameters invalid')); |
| } |
| out = Buffer.from(out); |
| var ckey = out.slice(0, cinf.keySize); |
| var iv = out.slice(cinf.keySize, cinf.keySize + cinf.blockSize); |
| |
| var cipherStream = crypto.createCipheriv(cinf.opensslName, |
| ckey, iv); |
| cipherStream.setAutoPadding(false); |
| var chunk, chunks = []; |
| cipherStream.once('error', function (e) { |
| throw (e); |
| }); |
| cipherStream.write(privBuf); |
| cipherStream.end(); |
| while ((chunk = cipherStream.read()) !== null) |
| chunks.push(chunk); |
| privBuf = Buffer.concat(chunks); |
| break; |
| default: |
| throw (new Error('Unsupported kdf ' + kdf)); |
| } |
| |
| var buf = new SSHBuffer({}); |
| |
| buf.writeCString(MAGIC); |
| buf.writeString(cipher); /* cipher */ |
| buf.writeString(kdf); /* kdf */ |
| buf.writeBuffer(kdfopts); /* kdfoptions */ |
| |
| buf.writeInt(1); /* nkeys */ |
| buf.writeBuffer(pubKey.toBuffer('rfc4253')); |
| |
| if (privBuf) |
| buf.writeBuffer(privBuf); |
| |
| buf = buf.toBuffer(); |
| |
| var header; |
| if (PrivateKey.isPrivateKey(key)) |
| header = 'OPENSSH PRIVATE KEY'; |
| else |
| header = 'OPENSSH PUBLIC KEY'; |
| |
| var tmp = buf.toString('base64'); |
| var len = tmp.length + (tmp.length / 70) + |
| 18 + 16 + header.length*2 + 10; |
| buf = Buffer.alloc(len); |
| var o = 0; |
| o += buf.write('-----BEGIN ' + header + '-----\n', o); |
| for (var i = 0; i < tmp.length; ) { |
| var limit = i + 70; |
| if (limit > tmp.length) |
| limit = tmp.length; |
| o += buf.write(tmp.slice(i, limit), o); |
| buf[o++] = 10; |
| i = limit; |
| } |
| o += buf.write('-----END ' + header + '-----\n', o); |
| |
| return (buf.slice(0, o)); |
| } |