Skip to content

Commit

Permalink
#18 support for PKCS8 encrypted private keys
Browse files Browse the repository at this point in the history
Reviewed by: Isaac Davis <[email protected]>
  • Loading branch information
Alex Wilson committed Dec 21, 2018
1 parent f647cf2 commit 574ff21
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 4 deletions.
94 changes: 91 additions & 3 deletions lib/formats/pem.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,29 @@ var rfc4253 = require('./rfc4253');

var errors = require('../errors');

var OID_PBES2 = '1.2.840.113549.1.5.13';
var OID_PBKDF2 = '1.2.840.113549.1.5.12';

var OID_TO_CIPHER = {
'1.2.840.113549.3.7': '3des-cbc',
'2.16.840.1.101.3.4.1.2': 'aes128-cbc',
'2.16.840.1.101.3.4.1.42': 'aes256-cbc'
};
var CIPHER_TO_OID = {};
Object.keys(OID_TO_CIPHER).forEach(function (k) {
CIPHER_TO_OID[OID_TO_CIPHER[k]] = k;
});

var OID_TO_HASH = {
'1.2.840.113549.2.7': 'sha1',
'1.2.840.113549.2.9': 'sha256',
'1.2.840.113549.2.11': 'sha512'
};
var HASH_TO_OID = {};
Object.keys(OID_TO_HASH).forEach(function (k) {
HASH_TO_OID[OID_TO_HASH[k]] = k;
});

/*
* For reading we support both PKCS#1 and PKCS#8. If we find a private key,
* we just take the public component of it and use that.
Expand Down Expand Up @@ -73,6 +96,10 @@ function read(buf, options, forceType) {
headers[m[1].toLowerCase()] = m[2];
}

/* Chop off the first and last lines */
lines = lines.slice(0, -1).join('');
buf = Buffer.from(lines, 'base64');

var cipher, key, iv;
if (headers['proc-type']) {
var parts = headers['proc-type'].split(',');
Expand All @@ -95,9 +122,70 @@ function read(buf, options, forceType) {
}
}

/* Chop off the first and last lines */
lines = lines.slice(0, -1).join('');
buf = Buffer.from(lines, 'base64');
if (alg && alg.toLowerCase() === 'encrypted') {
var eder = new asn1.BerReader(buf);
var pbesEnd;
eder.readSequence();

eder.readSequence();
pbesEnd = eder.offset + eder.length;

var method = eder.readOID();
if (method !== OID_PBES2) {
throw (new Error('Unsupported PEM/PKCS8 encryption ' +
'scheme: ' + method));
}

eder.readSequence(); /* PBES2-params */

eder.readSequence(); /* keyDerivationFunc */
var kdfEnd = eder.offset + eder.length;
var kdfOid = eder.readOID();
if (kdfOid !== OID_PBKDF2)
throw (new Error('Unsupported PBES2 KDF: ' + kdfOid));
eder.readSequence();
var salt = eder.readString(asn1.Ber.OctetString, true);
var iterations = eder.readInt();
var hashAlg = 'sha1';
if (eder.offset < kdfEnd) {
eder.readSequence();
var hashAlgOid = eder.readOID();
hashAlg = OID_TO_HASH[hashAlgOid];
if (hashAlg === undefined) {
throw (new Error('Unsupported PBKDF2 hash: ' +
hashAlgOid));
}
}
eder._offset = kdfEnd;

eder.readSequence(); /* encryptionScheme */
var cipherOid = eder.readOID();
cipher = OID_TO_CIPHER[cipherOid];
if (cipher === undefined) {
throw (new Error('Unsupported PBES2 cipher: ' +
cipherOid));
}
iv = eder.readString(asn1.Ber.OctetString, true);

eder._offset = pbesEnd;
buf = eder.readString(asn1.Ber.OctetString, true);

if (typeof (options.passphrase) === 'string') {
options.passphrase = Buffer.from(
options.passphrase, 'utf-8');
}
if (!Buffer.isBuffer(options.passphrase)) {
throw (new errors.KeyEncryptedError(
options.filename, 'PEM'));
}

var cinfo = utils.opensshCipherInfo(cipher);

cipher = cinfo.opensslName;
key = utils.pbkdf2(hashAlg, salt, iterations, cinfo.keySize,
options.passphrase);
alg = undefined;
}

if (cipher && key && iv) {
var cipherStream = crypto.createDecipheriv(cipher, key, iv);
Expand Down
37 changes: 36 additions & 1 deletion lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ module.exports = {
publicFromPrivateECDSA: publicFromPrivateECDSA,
zeroPadToLength: zeroPadToLength,
writeBitString: writeBitString,
readBitString: readBitString
readBitString: readBitString,
pbkdf2: pbkdf2
};

var assert = require('assert-plus');
Expand Down Expand Up @@ -124,6 +125,40 @@ function opensslKeyDeriv(cipher, salt, passphrase, count) {
});
}

/* See: RFC2898 */
function pbkdf2(hashAlg, salt, iterations, size, passphrase) {
var hkey = Buffer.alloc(salt.length + 4);
salt.copy(hkey);

var gen = 0, ts = [];
var i = 1;
while (gen < size) {
var t = T(i++);
gen += t.length;
ts.push(t);
}
return (Buffer.concat(ts).slice(0, size));

function T(I) {
hkey.writeUInt32BE(I, hkey.length - 4);

var hmac = crypto.createHmac(hashAlg, passphrase);
hmac.update(hkey);

var Ti = hmac.digest();
var Uc = Ti;
var c = 1;
while (c++ < iterations) {
hmac = crypto.createHmac(hashAlg, passphrase);
hmac.update(Uc);
Uc = hmac.digest();
for (var x = 0; x < Ti.length; ++x)
Ti[x] ^= Uc[x];
}
return (Ti);
}
}

/* Count leading zero bits on a buffer */
function countZeros(buf) {
var o = 0, obit = 8;
Expand Down
8 changes: 8 additions & 0 deletions test/assets/id_ecdsa_pkcs8_enc
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBEzBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI8ss6mjBMp84CAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPYbjSRfW4DmBIHAczs2+q5pZYOv
XHiGkWmOU7bAoLFbQy12vsswU/4wAZlm5/MNooJKQBx2cEDYKPQhhOoC6usARTnP
WaEqEkk++bTWc/JfQMJRHTWZwF7YIIA6J3VP11uRTyDiZeorGOr5qlUzwILZoT63
d+EUI7SfkQA6c2zEEYqZWYA+tB+ptztUsxmkiREfR3IOB3IWr63IbCsDmdt9/gny
47CRD5hnJxTkjWqsNBI8ZerlBgAjInKWx1vMkx7q+GZjZHcDp62r
-----END ENCRYPTED PRIVATE KEY-----
8 changes: 8 additions & 0 deletions test/assets/id_ecdsa_pkcs8_enc2
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBHDBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQILKyFq/D9ok4CAggA
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBIrV2g9Fy0mn+DhZKJN6EdBIHA
myZhhrYCTj58V3Je3Mn3H0dDY4o5rK1Mu+LOCFTQqjArO0FLsw6d/Xv5jmbT8Bq9
VZgmTrY1V+SHa1dVrEG4YnAXjorOVcEYxdUkpzkmJVl/vP2HYx0PFrL81+sDp2Ua
6SmX5ZE11Tq05y4oL+AQQJzAyf4BNAbUSjgd/83WE1pA4qeYE+nzEy26yrozWlLc
CJL8oq1DvubHOQX3HL4K68sU55M/i3tKLUFfz8e2KN0H8w3j8YgmF/pjn/t7kdAU
-----END ENCRYPTED PRIVATE KEY-----
8 changes: 8 additions & 0 deletions test/assets/id_ecdsa_pkcs8_enc3
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBDjBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQI7mY+aVq9o5gCAgKa
MB0GCWCGSAFlAwQBKgQQT5Y7S7LPoiJCYvKaOTKpIwSBwAdB+Y0Nr3YEkiQsOqMc
uLWM1QnVy7XOuv1ePOeU8oWZEp/YTX8xu1lRMrNOAdwXI99p+4aNCDEhyGPedi+7
6/fDsLD0NRpBchrRTJG2ZdTNF2ayABuDCoc39tGk1NwTNNQEJD1qIu46OJtbUca/
OfZBmuJS7JY7jZRwSpwyUlo9KfAn0ufleGQNOEF856uhVWjYt1ZPLhg0N+hC568+
w8Sk42ViF/kRid6EQI+1i4syqofWHSJHbLYxmqIS7Fv59Q==
-----END ENCRYPTED PRIVATE KEY-----
8 changes: 8 additions & 0 deletions test/assets/pkcs8-enc-bad-hash
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBHDBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQILKyFq/D9ok4CAggA
MAwGCCqGSIb3DQIQBQAwHQYJYIZIAWUDBAEqBBBIrV2g9Fy0mn+DhZKJN6EdBIHA
myZhhrYCTj58V3Je3Mn3H0dDY4o5rK1Mu+LOCFTQqjArO0FLsw6d/Xv5jmbT8Bq9
VZgmTrY1V+SHa1dVrEG4YnAXjorOVcEYxdUkpzkmJVl/vP2HYx0PFrL81+sDp2Ua
6SmX5ZE11Tq05y4oL+AQQJzAyf4BNAbUSjgd/83WE1pA4qeYE+nzEy26yrozWlLc
CJL8oq1DvubHOQX3HL4K68sU55M/i3tKLUFfz8e2KN0H8w3j8YgmF/pjn/t7kdAU
-----END ENCRYPTED PRIVATE KEY-----
8 changes: 8 additions & 0 deletions test/assets/pkcs8-enc-bad-iters
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBHDBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQILKyFq/D9ok4CAggB
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBIrV2g9Fy0mn+DhZKJN6EdBIHA
myZhhrYCTj58V3Je3Mn3H0dDY4o5rK1Mu+LOCFTQqjArO0FLsw6d/Xv5jmbT8Bq9
VZgmTrY1V+SHa1dVrEG4YnAXjorOVcEYxdUkpzkmJVl/vP2HYx0PFrL81+sDp2Ua
6SmX5ZE11Tq05y4oL+AQQJzAyf4BNAbUSjgd/83WE1pA4qeYE+nzEy26yrozWlLc
CJL8oq1DvubHOQX3HL4K68sU55M/i3tKLUFfz8e2KN0H8w3j8YgmF/pjn/t7kdAU
-----END ENCRYPTED PRIVATE KEY-----
8 changes: 8 additions & 0 deletions test/assets/pkcs8-enc-bad-kdf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBEzBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBAwwHAQI8ss6mjBMp84CAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPYbjSRfW4DmBIHAczs2+q5pZYOv
XHiGkWmOU7bAoLFbQy12vsswU/4wAZlm5/MNooJKQBx2cEDYKPQhhOoC6usARTnP
WaEqEkk++bTWc/JfQMJRHTWZwF7YIIA6J3VP11uRTyDiZeorGOr5qlUzwILZoT63
d+EUI7SfkQA6c2zEEYqZWYA+tB+ptztUsxmkiREfR3IOB3IWr63IbCsDmdt9/gny
47CRD5hnJxTkjWqsNBI8ZerlBgAjInKWx1vMkx7q+GZjZHcDp62r
-----END ENCRYPTED PRIVATE KEY-----
8 changes: 8 additions & 0 deletions test/assets/pkcs8-enc-bad-scheme
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-----BEGIN ENCRYPTED PRIVATE KEY-----
MIIBEzBOBgkqhkiG9w0BBQ4wQTApBgkqhkiG9w0BBQwwHAQI8ss6mjBMp84CAggA
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPYbjSRfW4DmBIHAczs2+q5pZYOv
XHiGkWmOU7bAoLFbQy12vsswU/4wAZlm5/MNooJKQBx2cEDYKPQhhOoC6usARTnP
WaEqEkk++bTWc/JfQMJRHTWZwF7YIIA6J3VP11uRTyDiZeorGOr5qlUzwILZoT63
d+EUI7SfkQA6c2zEEYqZWYA+tB+ptztUsxmkiREfR3IOB3IWr63IbCsDmdt9/gny
47CRD5hnJxTkjWqsNBI8ZerlBgAjInKWx1vMkx7q+GZjZHcDp62r
-----END ENCRYPTED PRIVATE KEY-----
51 changes: 51 additions & 0 deletions test/pem.js
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,54 @@ test('encrypted rsa private key (3des)', function (t) {
t.equal(key.toPublic().toString('ssh'), keySsh.trim());
t.end();
});

test('encrypted pkcs8 ecdsa private key (3des, pbkdf2 sha256)', function (t) {
var keyPem = fs.readFileSync(path.join(testDir, 'id_ecdsa_pkcs8_enc'));
var key = sshpk.parsePrivateKey(keyPem, 'pem',
{ passphrase: 'foobar' });
t.equal(key.type, 'ecdsa');
t.equal(key.fingerprint('sha256').toString(),
'SHA256:e34c67Npv31uMtfVUEBJln5aOcJugzDaYGsj1Uph5DE');
t.end();
});

test('encrypted pkcs8 ecdsa private key (aes256, pbkdf2 sha256)', function (t) {
var keyPem = fs.readFileSync(path.join(testDir, 'id_ecdsa_pkcs8_enc2'));
var key = sshpk.parsePrivateKey(keyPem, 'pem',
{ passphrase: 'testing123' });
t.equal(key.type, 'ecdsa');
t.equal(key.fingerprint('sha256').toString(),
'SHA256:e34c67Npv31uMtfVUEBJln5aOcJugzDaYGsj1Uph5DE');
t.end();
});

test('encrypted pkcs8 ecdsa private key (aes256, pbkdf2 sha1)', function (t) {
var keyPem = fs.readFileSync(path.join(testDir, 'id_ecdsa_pkcs8_enc3'));
var key = sshpk.parsePrivateKey(keyPem, 'pem',
{ passphrase: 'foobar123' });
t.equal(key.type, 'ecdsa');
t.equal(key.fingerprint('sha256').toString(),
'SHA256:e34c67Npv31uMtfVUEBJln5aOcJugzDaYGsj1Uph5DE');
t.end();
});

test('bad encrypted pkcs8 keys', function (t) {
var keyPem = fs.readFileSync(
path.join(testDir, 'pkcs8-enc-bad-scheme'));
t.throws(function () {
sshpk.parsePrivateKey(keyPem, 'pem', { passphrase: 'foobar' });
}, /unsupported pem\/pkcs8 encryption scheme/i);
keyPem = fs.readFileSync(path.join(testDir, 'pkcs8-enc-bad-kdf'));
t.throws(function () {
sshpk.parsePrivateKey(keyPem, 'pem', { passphrase: 'foobar' });
}, /unsupported pbes2 kdf/i);
keyPem = fs.readFileSync(path.join(testDir, 'pkcs8-enc-bad-hash'));
t.throws(function () {
sshpk.parsePrivateKey(keyPem, 'pem', { passphrase: 'foobar' });
}, /unsupported pbkdf2 hash/i);
keyPem = fs.readFileSync(path.join(testDir, 'pkcs8-enc-bad-iters'));
t.throws(function () {
sshpk.parsePrivateKey(keyPem, 'pem', { passphrase: 'foobar' });
}, /incorrect passphrase/i);
t.end();
});
54 changes: 54 additions & 0 deletions test/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,57 @@ test('bufferSplit multi char', function(t) {
t.equal(r[1].toString(), ' xyz ttt ');
t.end();
});

/* These taken from RFC6070 */
test('pbkdf2 test vector 1', function (t) {
var hashAlg = 'sha1';
var salt = Buffer.from('salt');
var iterations = 1;
var size = 20;
var passphrase = Buffer.from('password');

var key = utils.pbkdf2(hashAlg, salt, iterations, size, passphrase);
t.equal(key.toString('hex').toLowerCase(),
'0c60c80f961f0e71f3a9b524af6012062fe037a6');
t.end();
});

test('pbkdf2 test vector 2', function (t) {
var hashAlg = 'sha1';
var salt = Buffer.from('salt');
var iterations = 2;
var size = 20;
var passphrase = Buffer.from('password');

var key = utils.pbkdf2(hashAlg, salt, iterations, size, passphrase);
t.equal(key.toString('hex').toLowerCase(),
'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957');
t.end();
});

test('pbkdf2 test vector 5', function (t) {
var hashAlg = 'sha1';
var salt = Buffer.from('saltSALTsaltSALTsaltSALTsaltSALTsalt');
var iterations = 4096;
var size = 25;
var passphrase = Buffer.from('passwordPASSWORDpassword');

var key = utils.pbkdf2(hashAlg, salt, iterations, size, passphrase);
t.equal(key.toString('hex').toLowerCase(),
'3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038');
t.end();
});

test('pbkdf2 wiki test', function (t) {
var hashAlg = 'sha1';
var salt = Buffer.from('A009C1A485912C6AE630D3E744240B04', 'hex');
var iterations = 1000;
var size = 16;
var passphrase = Buffer.from(
'plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd');

var key = utils.pbkdf2(hashAlg, salt, iterations, size, passphrase);
t.equal(key.toString('hex').toUpperCase(),
'17EB4014C8C461C300E9B61518B9A18B');
t.end();
});

0 comments on commit 574ff21

Please sign in to comment.