Skip to content

Commit

Permalink
#13 support for encrypted OpenSSH format private keys
Browse files Browse the repository at this point in the history
Reviewed by: Cody Mello <cody.mello@joyent.com>
  • Loading branch information
arekinath committed Aug 29, 2016
1 parent a8b7943 commit f3c2972
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 22 deletions.
6 changes: 6 additions & 0 deletions bin/sshpk-conv
Expand Up @@ -149,6 +149,12 @@ if (require.main === module) {
} catch (e) {
if (e.name === 'KeyEncryptedError') {
getPassword(function (err, pw) {
if (err) {
console.log('sshpk-conv: ' +
err.name + ': ' +
err.message);
process.exit(1);
}
parseOpts.passphrase = pw;
processKey();
});
Expand Down
4 changes: 2 additions & 2 deletions lib/formats/pem.js
Expand Up @@ -107,9 +107,9 @@ function read(buf, options, forceType) {

/* The new OpenSSH internal format abuses PEM headers */
if (alg && alg.toLowerCase() === 'openssh')
return (sshpriv.readSSHPrivate(type, buf));
return (sshpriv.readSSHPrivate(type, buf, options));
if (alg && alg.toLowerCase() === 'ssh2')
return (rfc4253.readType(type, buf));
return (rfc4253.readType(type, buf, options));

var der = new asn1.BerReader(buf);
der.originalInput = input;
Expand Down
157 changes: 140 additions & 17 deletions lib/formats/ssh-private.js
Expand Up @@ -17,31 +17,25 @@ 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) {
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();

/* We only support unencrypted keys. */
if (cipher !== 'none' || kdf !== 'none') {
throw (new Error('OpenSSH-format key is encrypted ' +
'(password-protected). Please use the SSH agent ' +
'or decrypt the key.'));
}

/* Skip over kdfoptions. */
buf.readString();
var kdfOpts = buf.readBuffer();

var nkeys = buf.readInt();
if (nkeys !== 1) {
Expand All @@ -59,11 +53,74 @@ function readSSHPrivate(type, buf) {
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 = new Buffer(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 = new Buffer(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();
assert.strictEqual(checkInt1, checkInt2, 'checkints do not match');
if (checkInt1 !== checkInt2) {
throw (new Error('Incorrect passphrase supplied, could not ' +
'decrypt key'));
}

var ret = {};
var key = rfc4253.readInternal(ret, 'private', buf.remainder());
Expand All @@ -83,6 +140,26 @@ function write(key, options) {
else
pubKey = key;

var cipher = 'none';
var kdf = 'none';
var kdfopts = new Buffer(0);
var cinf = { blockSize: 8 };
var passphrase;
if (options !== undefined) {
passphrase = options.passphrase;
if (typeof (passphrase) === 'string')
passphrase = new Buffer(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({});
Expand All @@ -93,22 +170,68 @@ function write(key, options) {
privBuf.writeString(key.comment || '');

var n = 1;
while (privBuf._offset % 8 !== 0)
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 = new Buffer(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('none'); /* cipher */
buf.writeString('none'); /* kdf */
buf.writeBuffer(new Buffer(0)); /* kdfoptions */
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.toBuffer());
buf.writeBuffer(privBuf);

buf = buf.toBuffer();

Expand Down
44 changes: 43 additions & 1 deletion lib/utils.js
Expand Up @@ -9,7 +9,8 @@ module.exports = {
countZeros: countZeros,
assertCompatible: assertCompatible,
isCompatible: isCompatible,
opensslKeyDeriv: opensslKeyDeriv
opensslKeyDeriv: opensslKeyDeriv,
opensshCipherInfo: opensshCipherInfo
};

var assert = require('assert-plus');
Expand Down Expand Up @@ -244,3 +245,44 @@ function addRSAMissing(key) {
key.parts.push(key.part.dmodq);
}
}

function opensshCipherInfo(cipher) {
var inf = {};
switch (cipher) {
case '3des-cbc':
inf.keySize = 24;
inf.blockSize = 8;
inf.opensslName = 'des-ede3-cbc';
break;
case 'blowfish-cbc':
inf.keySize = 16;
inf.blockSize = 8;
inf.opensslName = 'bf-cbc';
break;
case 'aes128-cbc':
case 'aes128-ctr':
case 'aes128-gcm@openssh.com':
inf.keySize = 16;
inf.blockSize = 16;
inf.opensslName = 'aes-128-' + cipher.slice(7, 10);
break;
case 'aes192-cbc':
case 'aes192-ctr':
case 'aes192-gcm@openssh.com':
inf.keySize = 24;
inf.blockSize = 16;
inf.opensslName = 'aes-192-' + cipher.slice(7, 10);
break;
case 'aes256-cbc':
case 'aes256-ctr':
case 'aes256-gcm@openssh.com':
inf.keySize = 32;
inf.blockSize = 16;
inf.opensslName = 'aes-256-' + cipher.slice(7, 10);
break;
default:
throw (new Error(
'Unsupported openssl cipher "' + cipher + '"'));
}
return (inf);
}
5 changes: 3 additions & 2 deletions package.json
@@ -1,6 +1,6 @@
{
"name": "sshpk",
"version": "1.9.2",
"version": "1.10.0",
"description": "A library for finding and using SSH public keys",
"main": "lib/index.js",
"scripts": {
Expand Down Expand Up @@ -48,7 +48,8 @@
"jsbn": "~0.1.0",
"tweetnacl": "~0.13.0",
"jodid25519": "^1.0.0",
"ecc-jsbn": "~0.1.1"
"ecc-jsbn": "~0.1.1",
"bcrypt-pbkdf": "^1.0.0"
},
"devDependencies": {
"tape": "^3.5.0",
Expand Down
10 changes: 10 additions & 0 deletions test/assets/id_ecdsa_enc
@@ -0,0 +1,10 @@
-----BEGIN OPENSSH PRIVATE KEY-----
b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABCxRf8+2g
kOoRguCgCcgocnAAAAEAAAAAEAAABoAAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlz
dHAyNTYAAABBBJIH5wFP3F6cuSwYBr0L1PdH+sL3uNIwpLlXF3OaUUIt1omTUKaGZ79vFb
tIH5A78WRmEdLMLi6EA5Hy6AI5YNEAAADAJsEFTLkiT/A2Vfer0iK3rGtvNrjuuuYGS8VV
xRenc9N4QFNtUHMbNoqTriyLplAU5/LwEAJ9kXtdCkvcpFjW3h6OqG9ttvSiyrfk/84ULG
raqvAuBdyEK6T8iuo4f62r7kdJxGQMJM52LtKaU/E2aPFadwTDfeQY8W53AFKIplHyG4Hj
5LOSvoDzkMZgMxqCLHyEqAenPwj9OjIJ7ff60Mk6dJs+RmFynAEGYI3d3oviIRNvYJdjhv
lAAFohORH6
-----END OPENSSH PRIVATE KEY-----
31 changes: 31 additions & 0 deletions test/private-key.js
Expand Up @@ -19,6 +19,8 @@ var ID_ED25519_FP = sshpk.parseFingerprint(
'SHA256:2UeFLCUKw2lvd8O1zfINNVzE0kUcu2HJHXQr/TGHt60');
var ID_RSA_O_FP = sshpk.parseFingerprint(
'SHA256:sfZqx0wyXwuXhsza0Ld99+/YNEMFyubTD8fPJ1Jo7Xw');
var ID_ECDSA_ENC_FP = sshpk.parseFingerprint(
'SHA256:n2/53LRiEy+DBbKltRHQC36vwRndRJve+912b8zDvow');

test('PrivateKey load RSA key', function (t) {
var keyPem = fs.readFileSync(path.join(testDir, 'id_rsa'));
Expand Down Expand Up @@ -112,6 +114,35 @@ test('PrivateKey convert ssh-private rsa to pem', function (t) {
t.end();
});

test('parse and produce encrypted ssh-private ecdsa', function (t) {
var keySsh = fs.readFileSync(path.join(testDir, 'id_ecdsa_enc'));
t.throws(function () {
sshpk.parsePrivateKey(keySsh, 'ssh-private');
});
t.throws(function () {
sshpk.parsePrivateKey(keySsh, 'ssh-private',
{ passphrase: 'incorrect' });
});
var key = sshpk.parsePrivateKey(keySsh, 'ssh-private',
{ passphrase: 'foobar' });
t.strictEqual(key.type, 'ecdsa');
t.strictEqual(key.size, 256);
t.ok(ID_ECDSA_ENC_FP.matches(key));

var keySsh2 = key.toBuffer('ssh-private', { passphrase: 'foobar2' });
t.throws(function () {
sshpk.parsePrivateKey(keySsh2, 'ssh-private',
{ passphrase: 'foobar' });
});
var key2 = sshpk.parsePrivateKey(keySsh2, 'ssh-private',
{ passphrase: 'foobar2' });
t.strictEqual(key.type, 'ecdsa');
t.strictEqual(key.size, 256);
t.ok(ID_ECDSA_ENC_FP.matches(key));

t.end();
});

var KEY_RSA, KEY_DSA, KEY_ECDSA, KEY_ECDSA2, KEY_ED25519;

test('setup keys', function (t) {
Expand Down

0 comments on commit f3c2972

Please sign in to comment.