Skip to content

Commit 574ff21

Browse files
author
Alex Wilson
committed
#18 support for PKCS8 encrypted private keys
Reviewed by: Isaac Davis <[email protected]>
1 parent f647cf2 commit 574ff21

11 files changed

+288
-4
lines changed

lib/formats/pem.js

+91-3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,29 @@ var rfc4253 = require('./rfc4253');
2121

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

24+
var OID_PBES2 = '1.2.840.113549.1.5.13';
25+
var OID_PBKDF2 = '1.2.840.113549.1.5.12';
26+
27+
var OID_TO_CIPHER = {
28+
'1.2.840.113549.3.7': '3des-cbc',
29+
'2.16.840.1.101.3.4.1.2': 'aes128-cbc',
30+
'2.16.840.1.101.3.4.1.42': 'aes256-cbc'
31+
};
32+
var CIPHER_TO_OID = {};
33+
Object.keys(OID_TO_CIPHER).forEach(function (k) {
34+
CIPHER_TO_OID[OID_TO_CIPHER[k]] = k;
35+
});
36+
37+
var OID_TO_HASH = {
38+
'1.2.840.113549.2.7': 'sha1',
39+
'1.2.840.113549.2.9': 'sha256',
40+
'1.2.840.113549.2.11': 'sha512'
41+
};
42+
var HASH_TO_OID = {};
43+
Object.keys(OID_TO_HASH).forEach(function (k) {
44+
HASH_TO_OID[OID_TO_HASH[k]] = k;
45+
});
46+
2447
/*
2548
* For reading we support both PKCS#1 and PKCS#8. If we find a private key,
2649
* we just take the public component of it and use that.
@@ -73,6 +96,10 @@ function read(buf, options, forceType) {
7396
headers[m[1].toLowerCase()] = m[2];
7497
}
7598

99+
/* Chop off the first and last lines */
100+
lines = lines.slice(0, -1).join('');
101+
buf = Buffer.from(lines, 'base64');
102+
76103
var cipher, key, iv;
77104
if (headers['proc-type']) {
78105
var parts = headers['proc-type'].split(',');
@@ -95,9 +122,70 @@ function read(buf, options, forceType) {
95122
}
96123
}
97124

98-
/* Chop off the first and last lines */
99-
lines = lines.slice(0, -1).join('');
100-
buf = Buffer.from(lines, 'base64');
125+
if (alg && alg.toLowerCase() === 'encrypted') {
126+
var eder = new asn1.BerReader(buf);
127+
var pbesEnd;
128+
eder.readSequence();
129+
130+
eder.readSequence();
131+
pbesEnd = eder.offset + eder.length;
132+
133+
var method = eder.readOID();
134+
if (method !== OID_PBES2) {
135+
throw (new Error('Unsupported PEM/PKCS8 encryption ' +
136+
'scheme: ' + method));
137+
}
138+
139+
eder.readSequence(); /* PBES2-params */
140+
141+
eder.readSequence(); /* keyDerivationFunc */
142+
var kdfEnd = eder.offset + eder.length;
143+
var kdfOid = eder.readOID();
144+
if (kdfOid !== OID_PBKDF2)
145+
throw (new Error('Unsupported PBES2 KDF: ' + kdfOid));
146+
eder.readSequence();
147+
var salt = eder.readString(asn1.Ber.OctetString, true);
148+
var iterations = eder.readInt();
149+
var hashAlg = 'sha1';
150+
if (eder.offset < kdfEnd) {
151+
eder.readSequence();
152+
var hashAlgOid = eder.readOID();
153+
hashAlg = OID_TO_HASH[hashAlgOid];
154+
if (hashAlg === undefined) {
155+
throw (new Error('Unsupported PBKDF2 hash: ' +
156+
hashAlgOid));
157+
}
158+
}
159+
eder._offset = kdfEnd;
160+
161+
eder.readSequence(); /* encryptionScheme */
162+
var cipherOid = eder.readOID();
163+
cipher = OID_TO_CIPHER[cipherOid];
164+
if (cipher === undefined) {
165+
throw (new Error('Unsupported PBES2 cipher: ' +
166+
cipherOid));
167+
}
168+
iv = eder.readString(asn1.Ber.OctetString, true);
169+
170+
eder._offset = pbesEnd;
171+
buf = eder.readString(asn1.Ber.OctetString, true);
172+
173+
if (typeof (options.passphrase) === 'string') {
174+
options.passphrase = Buffer.from(
175+
options.passphrase, 'utf-8');
176+
}
177+
if (!Buffer.isBuffer(options.passphrase)) {
178+
throw (new errors.KeyEncryptedError(
179+
options.filename, 'PEM'));
180+
}
181+
182+
var cinfo = utils.opensshCipherInfo(cipher);
183+
184+
cipher = cinfo.opensslName;
185+
key = utils.pbkdf2(hashAlg, salt, iterations, cinfo.keySize,
186+
options.passphrase);
187+
alg = undefined;
188+
}
101189

102190
if (cipher && key && iv) {
103191
var cipherStream = crypto.createDecipheriv(cipher, key, iv);

lib/utils.js

+36-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ module.exports = {
1717
publicFromPrivateECDSA: publicFromPrivateECDSA,
1818
zeroPadToLength: zeroPadToLength,
1919
writeBitString: writeBitString,
20-
readBitString: readBitString
20+
readBitString: readBitString,
21+
pbkdf2: pbkdf2
2122
};
2223

2324
var assert = require('assert-plus');
@@ -124,6 +125,40 @@ function opensslKeyDeriv(cipher, salt, passphrase, count) {
124125
});
125126
}
126127

128+
/* See: RFC2898 */
129+
function pbkdf2(hashAlg, salt, iterations, size, passphrase) {
130+
var hkey = Buffer.alloc(salt.length + 4);
131+
salt.copy(hkey);
132+
133+
var gen = 0, ts = [];
134+
var i = 1;
135+
while (gen < size) {
136+
var t = T(i++);
137+
gen += t.length;
138+
ts.push(t);
139+
}
140+
return (Buffer.concat(ts).slice(0, size));
141+
142+
function T(I) {
143+
hkey.writeUInt32BE(I, hkey.length - 4);
144+
145+
var hmac = crypto.createHmac(hashAlg, passphrase);
146+
hmac.update(hkey);
147+
148+
var Ti = hmac.digest();
149+
var Uc = Ti;
150+
var c = 1;
151+
while (c++ < iterations) {
152+
hmac = crypto.createHmac(hashAlg, passphrase);
153+
hmac.update(Uc);
154+
Uc = hmac.digest();
155+
for (var x = 0; x < Ti.length; ++x)
156+
Ti[x] ^= Uc[x];
157+
}
158+
return (Ti);
159+
}
160+
}
161+
127162
/* Count leading zero bits on a buffer */
128163
function countZeros(buf) {
129164
var o = 0, obit = 8;

test/assets/id_ecdsa_pkcs8_enc

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIIBEzBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBQwwHAQI8ss6mjBMp84CAggA
3+
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPYbjSRfW4DmBIHAczs2+q5pZYOv
4+
XHiGkWmOU7bAoLFbQy12vsswU/4wAZlm5/MNooJKQBx2cEDYKPQhhOoC6usARTnP
5+
WaEqEkk++bTWc/JfQMJRHTWZwF7YIIA6J3VP11uRTyDiZeorGOr5qlUzwILZoT63
6+
d+EUI7SfkQA6c2zEEYqZWYA+tB+ptztUsxmkiREfR3IOB3IWr63IbCsDmdt9/gny
7+
47CRD5hnJxTkjWqsNBI8ZerlBgAjInKWx1vMkx7q+GZjZHcDp62r
8+
-----END ENCRYPTED PRIVATE KEY-----

test/assets/id_ecdsa_pkcs8_enc2

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIIBHDBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQILKyFq/D9ok4CAggA
3+
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBIrV2g9Fy0mn+DhZKJN6EdBIHA
4+
myZhhrYCTj58V3Je3Mn3H0dDY4o5rK1Mu+LOCFTQqjArO0FLsw6d/Xv5jmbT8Bq9
5+
VZgmTrY1V+SHa1dVrEG4YnAXjorOVcEYxdUkpzkmJVl/vP2HYx0PFrL81+sDp2Ua
6+
6SmX5ZE11Tq05y4oL+AQQJzAyf4BNAbUSjgd/83WE1pA4qeYE+nzEy26yrozWlLc
7+
CJL8oq1DvubHOQX3HL4K68sU55M/i3tKLUFfz8e2KN0H8w3j8YgmF/pjn/t7kdAU
8+
-----END ENCRYPTED PRIVATE KEY-----

test/assets/id_ecdsa_pkcs8_enc3

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIIBDjBJBgkqhkiG9w0BBQ0wPDAbBgkqhkiG9w0BBQwwDgQI7mY+aVq9o5gCAgKa
3+
MB0GCWCGSAFlAwQBKgQQT5Y7S7LPoiJCYvKaOTKpIwSBwAdB+Y0Nr3YEkiQsOqMc
4+
uLWM1QnVy7XOuv1ePOeU8oWZEp/YTX8xu1lRMrNOAdwXI99p+4aNCDEhyGPedi+7
5+
6/fDsLD0NRpBchrRTJG2ZdTNF2ayABuDCoc39tGk1NwTNNQEJD1qIu46OJtbUca/
6+
OfZBmuJS7JY7jZRwSpwyUlo9KfAn0ufleGQNOEF856uhVWjYt1ZPLhg0N+hC568+
7+
w8Sk42ViF/kRid6EQI+1i4syqofWHSJHbLYxmqIS7Fv59Q==
8+
-----END ENCRYPTED PRIVATE KEY-----

test/assets/pkcs8-enc-bad-hash

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIIBHDBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQILKyFq/D9ok4CAggA
3+
MAwGCCqGSIb3DQIQBQAwHQYJYIZIAWUDBAEqBBBIrV2g9Fy0mn+DhZKJN6EdBIHA
4+
myZhhrYCTj58V3Je3Mn3H0dDY4o5rK1Mu+LOCFTQqjArO0FLsw6d/Xv5jmbT8Bq9
5+
VZgmTrY1V+SHa1dVrEG4YnAXjorOVcEYxdUkpzkmJVl/vP2HYx0PFrL81+sDp2Ua
6+
6SmX5ZE11Tq05y4oL+AQQJzAyf4BNAbUSjgd/83WE1pA4qeYE+nzEy26yrozWlLc
7+
CJL8oq1DvubHOQX3HL4K68sU55M/i3tKLUFfz8e2KN0H8w3j8YgmF/pjn/t7kdAU
8+
-----END ENCRYPTED PRIVATE KEY-----

test/assets/pkcs8-enc-bad-iters

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIIBHDBXBgkqhkiG9w0BBQ0wSjApBgkqhkiG9w0BBQwwHAQILKyFq/D9ok4CAggB
3+
MAwGCCqGSIb3DQIJBQAwHQYJYIZIAWUDBAEqBBBIrV2g9Fy0mn+DhZKJN6EdBIHA
4+
myZhhrYCTj58V3Je3Mn3H0dDY4o5rK1Mu+LOCFTQqjArO0FLsw6d/Xv5jmbT8Bq9
5+
VZgmTrY1V+SHa1dVrEG4YnAXjorOVcEYxdUkpzkmJVl/vP2HYx0PFrL81+sDp2Ua
6+
6SmX5ZE11Tq05y4oL+AQQJzAyf4BNAbUSjgd/83WE1pA4qeYE+nzEy26yrozWlLc
7+
CJL8oq1DvubHOQX3HL4K68sU55M/i3tKLUFfz8e2KN0H8w3j8YgmF/pjn/t7kdAU
8+
-----END ENCRYPTED PRIVATE KEY-----

test/assets/pkcs8-enc-bad-kdf

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIIBEzBOBgkqhkiG9w0BBQ0wQTApBgkqhkiG9w0BBAwwHAQI8ss6mjBMp84CAggA
3+
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPYbjSRfW4DmBIHAczs2+q5pZYOv
4+
XHiGkWmOU7bAoLFbQy12vsswU/4wAZlm5/MNooJKQBx2cEDYKPQhhOoC6usARTnP
5+
WaEqEkk++bTWc/JfQMJRHTWZwF7YIIA6J3VP11uRTyDiZeorGOr5qlUzwILZoT63
6+
d+EUI7SfkQA6c2zEEYqZWYA+tB+ptztUsxmkiREfR3IOB3IWr63IbCsDmdt9/gny
7+
47CRD5hnJxTkjWqsNBI8ZerlBgAjInKWx1vMkx7q+GZjZHcDp62r
8+
-----END ENCRYPTED PRIVATE KEY-----

test/assets/pkcs8-enc-bad-scheme

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
-----BEGIN ENCRYPTED PRIVATE KEY-----
2+
MIIBEzBOBgkqhkiG9w0BBQ4wQTApBgkqhkiG9w0BBQwwHAQI8ss6mjBMp84CAggA
3+
MAwGCCqGSIb3DQIJBQAwFAYIKoZIhvcNAwcECPYbjSRfW4DmBIHAczs2+q5pZYOv
4+
XHiGkWmOU7bAoLFbQy12vsswU/4wAZlm5/MNooJKQBx2cEDYKPQhhOoC6usARTnP
5+
WaEqEkk++bTWc/JfQMJRHTWZwF7YIIA6J3VP11uRTyDiZeorGOr5qlUzwILZoT63
6+
d+EUI7SfkQA6c2zEEYqZWYA+tB+ptztUsxmkiREfR3IOB3IWr63IbCsDmdt9/gny
7+
47CRD5hnJxTkjWqsNBI8ZerlBgAjInKWx1vMkx7q+GZjZHcDp62r
8+
-----END ENCRYPTED PRIVATE KEY-----

test/pem.js

+51
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,54 @@ test('encrypted rsa private key (3des)', function (t) {
356356
t.equal(key.toPublic().toString('ssh'), keySsh.trim());
357357
t.end();
358358
});
359+
360+
test('encrypted pkcs8 ecdsa private key (3des, pbkdf2 sha256)', function (t) {
361+
var keyPem = fs.readFileSync(path.join(testDir, 'id_ecdsa_pkcs8_enc'));
362+
var key = sshpk.parsePrivateKey(keyPem, 'pem',
363+
{ passphrase: 'foobar' });
364+
t.equal(key.type, 'ecdsa');
365+
t.equal(key.fingerprint('sha256').toString(),
366+
'SHA256:e34c67Npv31uMtfVUEBJln5aOcJugzDaYGsj1Uph5DE');
367+
t.end();
368+
});
369+
370+
test('encrypted pkcs8 ecdsa private key (aes256, pbkdf2 sha256)', function (t) {
371+
var keyPem = fs.readFileSync(path.join(testDir, 'id_ecdsa_pkcs8_enc2'));
372+
var key = sshpk.parsePrivateKey(keyPem, 'pem',
373+
{ passphrase: 'testing123' });
374+
t.equal(key.type, 'ecdsa');
375+
t.equal(key.fingerprint('sha256').toString(),
376+
'SHA256:e34c67Npv31uMtfVUEBJln5aOcJugzDaYGsj1Uph5DE');
377+
t.end();
378+
});
379+
380+
test('encrypted pkcs8 ecdsa private key (aes256, pbkdf2 sha1)', function (t) {
381+
var keyPem = fs.readFileSync(path.join(testDir, 'id_ecdsa_pkcs8_enc3'));
382+
var key = sshpk.parsePrivateKey(keyPem, 'pem',
383+
{ passphrase: 'foobar123' });
384+
t.equal(key.type, 'ecdsa');
385+
t.equal(key.fingerprint('sha256').toString(),
386+
'SHA256:e34c67Npv31uMtfVUEBJln5aOcJugzDaYGsj1Uph5DE');
387+
t.end();
388+
});
389+
390+
test('bad encrypted pkcs8 keys', function (t) {
391+
var keyPem = fs.readFileSync(
392+
path.join(testDir, 'pkcs8-enc-bad-scheme'));
393+
t.throws(function () {
394+
sshpk.parsePrivateKey(keyPem, 'pem', { passphrase: 'foobar' });
395+
}, /unsupported pem\/pkcs8 encryption scheme/i);
396+
keyPem = fs.readFileSync(path.join(testDir, 'pkcs8-enc-bad-kdf'));
397+
t.throws(function () {
398+
sshpk.parsePrivateKey(keyPem, 'pem', { passphrase: 'foobar' });
399+
}, /unsupported pbes2 kdf/i);
400+
keyPem = fs.readFileSync(path.join(testDir, 'pkcs8-enc-bad-hash'));
401+
t.throws(function () {
402+
sshpk.parsePrivateKey(keyPem, 'pem', { passphrase: 'foobar' });
403+
}, /unsupported pbkdf2 hash/i);
404+
keyPem = fs.readFileSync(path.join(testDir, 'pkcs8-enc-bad-iters'));
405+
t.throws(function () {
406+
sshpk.parsePrivateKey(keyPem, 'pem', { passphrase: 'foobar' });
407+
}, /incorrect passphrase/i);
408+
t.end();
409+
});

test/utils.js

+54
Original file line numberDiff line numberDiff line change
@@ -34,3 +34,57 @@ test('bufferSplit multi char', function(t) {
3434
t.equal(r[1].toString(), ' xyz ttt ');
3535
t.end();
3636
});
37+
38+
/* These taken from RFC6070 */
39+
test('pbkdf2 test vector 1', function (t) {
40+
var hashAlg = 'sha1';
41+
var salt = Buffer.from('salt');
42+
var iterations = 1;
43+
var size = 20;
44+
var passphrase = Buffer.from('password');
45+
46+
var key = utils.pbkdf2(hashAlg, salt, iterations, size, passphrase);
47+
t.equal(key.toString('hex').toLowerCase(),
48+
'0c60c80f961f0e71f3a9b524af6012062fe037a6');
49+
t.end();
50+
});
51+
52+
test('pbkdf2 test vector 2', function (t) {
53+
var hashAlg = 'sha1';
54+
var salt = Buffer.from('salt');
55+
var iterations = 2;
56+
var size = 20;
57+
var passphrase = Buffer.from('password');
58+
59+
var key = utils.pbkdf2(hashAlg, salt, iterations, size, passphrase);
60+
t.equal(key.toString('hex').toLowerCase(),
61+
'ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957');
62+
t.end();
63+
});
64+
65+
test('pbkdf2 test vector 5', function (t) {
66+
var hashAlg = 'sha1';
67+
var salt = Buffer.from('saltSALTsaltSALTsaltSALTsaltSALTsalt');
68+
var iterations = 4096;
69+
var size = 25;
70+
var passphrase = Buffer.from('passwordPASSWORDpassword');
71+
72+
var key = utils.pbkdf2(hashAlg, salt, iterations, size, passphrase);
73+
t.equal(key.toString('hex').toLowerCase(),
74+
'3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038');
75+
t.end();
76+
});
77+
78+
test('pbkdf2 wiki test', function (t) {
79+
var hashAlg = 'sha1';
80+
var salt = Buffer.from('A009C1A485912C6AE630D3E744240B04', 'hex');
81+
var iterations = 1000;
82+
var size = 16;
83+
var passphrase = Buffer.from(
84+
'plnlrtfpijpuhqylxbgqiiyipieyxvfsavzgxbbcfusqkozwpngsyejqlmjsytrmd');
85+
86+
var key = utils.pbkdf2(hashAlg, salt, iterations, size, passphrase);
87+
t.equal(key.toString('hex').toUpperCase(),
88+
'17EB4014C8C461C300E9B61518B9A18B');
89+
t.end();
90+
});

0 commit comments

Comments
 (0)