From 574ff21e77117b478baf4664856bfc4b0aa41a12 Mon Sep 17 00:00:00 2001 From: Alex Wilson Date: Wed, 19 Dec 2018 16:53:48 -0800 Subject: [PATCH] joyent/node-sshpk#18 support for PKCS8 encrypted private keys Reviewed by: Isaac Davis --- lib/formats/pem.js | 94 +++++++++++++++++++++++++++++++- lib/utils.js | 37 ++++++++++++- test/assets/id_ecdsa_pkcs8_enc | 8 +++ test/assets/id_ecdsa_pkcs8_enc2 | 8 +++ test/assets/id_ecdsa_pkcs8_enc3 | 8 +++ test/assets/pkcs8-enc-bad-hash | 8 +++ test/assets/pkcs8-enc-bad-iters | 8 +++ test/assets/pkcs8-enc-bad-kdf | 8 +++ test/assets/pkcs8-enc-bad-scheme | 8 +++ test/pem.js | 51 +++++++++++++++++ test/utils.js | 54 ++++++++++++++++++ 11 files changed, 288 insertions(+), 4 deletions(-) create mode 100644 test/assets/id_ecdsa_pkcs8_enc create mode 100644 test/assets/id_ecdsa_pkcs8_enc2 create mode 100644 test/assets/id_ecdsa_pkcs8_enc3 create mode 100644 test/assets/pkcs8-enc-bad-hash create mode 100644 test/assets/pkcs8-enc-bad-iters create mode 100644 test/assets/pkcs8-enc-bad-kdf create mode 100644 test/assets/pkcs8-enc-bad-scheme diff --git a/lib/formats/pem.js b/lib/formats/pem.js index b43e472..bbe78fc 100644 --- a/lib/formats/pem.js +++ b/lib/formats/pem.js @@ -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. @@ -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(','); @@ -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); diff --git a/lib/utils.js b/lib/utils.js index ecaeb0a..6b83a32 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -17,7 +17,8 @@ module.exports = { publicFromPrivateECDSA: publicFromPrivateECDSA, zeroPadToLength: zeroPadToLength, writeBitString: writeBitString, - readBitString: readBitString + readBitString: readBitString, + pbkdf2: pbkdf2 }; var assert = require('assert-plus'); @@ -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; diff --git a/test/assets/id_ecdsa_pkcs8_enc b/test/assets/id_ecdsa_pkcs8_enc new file mode 100644 index 0000000..15ded91 --- /dev/null +++ b/test/assets/id_ecdsa_pkcs8_enc @@ -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----- diff --git a/test/assets/id_ecdsa_pkcs8_enc2 b/test/assets/id_ecdsa_pkcs8_enc2 new file mode 100644 index 0000000..c6658a4 --- /dev/null +++ b/test/assets/id_ecdsa_pkcs8_enc2 @@ -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----- diff --git a/test/assets/id_ecdsa_pkcs8_enc3 b/test/assets/id_ecdsa_pkcs8_enc3 new file mode 100644 index 0000000..a7d4f14 --- /dev/null +++ b/test/assets/id_ecdsa_pkcs8_enc3 @@ -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----- diff --git a/test/assets/pkcs8-enc-bad-hash b/test/assets/pkcs8-enc-bad-hash new file mode 100644 index 0000000..fc008f2 --- /dev/null +++ b/test/assets/pkcs8-enc-bad-hash @@ -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----- diff --git a/test/assets/pkcs8-enc-bad-iters b/test/assets/pkcs8-enc-bad-iters new file mode 100644 index 0000000..dc8e8e0 --- /dev/null +++ b/test/assets/pkcs8-enc-bad-iters @@ -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----- diff --git a/test/assets/pkcs8-enc-bad-kdf b/test/assets/pkcs8-enc-bad-kdf new file mode 100644 index 0000000..26fa009 --- /dev/null +++ b/test/assets/pkcs8-enc-bad-kdf @@ -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----- diff --git a/test/assets/pkcs8-enc-bad-scheme b/test/assets/pkcs8-enc-bad-scheme new file mode 100644 index 0000000..a5d2495 --- /dev/null +++ b/test/assets/pkcs8-enc-bad-scheme @@ -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----- diff --git a/test/pem.js b/test/pem.js index 3aa6e6d..0c9811a 100644 --- a/test/pem.js +++ b/test/pem.js @@ -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(); +}); diff --git a/test/utils.js b/test/utils.js index 3f00698..d20a0f1 100644 --- a/test/utils.js +++ b/test/utils.js @@ -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(); +});