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.
262 lines
6.8 KiB
262 lines
6.8 KiB
// 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)); |
|
}
|
|
|