From 6d15450438fd06aed30754f6afcd3c0e87486fdc Mon Sep 17 00:00:00 2001 From: Friedel Ziegelmayer Date: Tue, 29 Nov 2016 16:36:56 +0100 Subject: [PATCH] feat(ecdh): use node core instead of webcrypto-ossl --- package.json | 3 +- src/crypto/ecdh-browser.js | 129 +++++++++++++++++++++++++++++++ src/crypto/ecdh.js | 120 +++++++--------------------- test/ephemeral-keys.spec.js | 30 ++++++- test/fixtures/go-elliptic-key.js | 4 +- 5 files changed, 189 insertions(+), 97 deletions(-) create mode 100644 src/crypto/ecdh-browser.js diff --git a/package.json b/package.json index 013cc72..c203f7f 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "node-webcrypto-ossl": false, "./src/crypto/webcrypto.js": "./src/crypto/webcrypto-browser.js", "./src/crypto/hmac.js": "./src/crypto/hmac-browser.js", + "./src/crypto/ecdh.js": "./src/crypto/ecdh-browser.js", "./src/crypto/ciphers.js": "./src/crypto/ciphers-browser.js" }, "scripts": { @@ -67,4 +68,4 @@ "greenkeeperio-bot ", "nikuda " ] -} \ No newline at end of file +} diff --git a/src/crypto/ecdh-browser.js b/src/crypto/ecdh-browser.js new file mode 100644 index 0000000..1970e1b --- /dev/null +++ b/src/crypto/ecdh-browser.js @@ -0,0 +1,129 @@ +'use strict' + +const crypto = require('./webcrypto')() +const nodeify = require('nodeify') +const BN = require('asn1.js').bignum + +const util = require('./util') +const toBase64 = util.toBase64 +const toBn = util.toBn + +const bits = { + 'P-256': 256, + 'P-384': 384, + 'P-521': 521 +} + +exports.generateEphmeralKeyPair = function (curve, callback) { + nodeify(crypto.subtle.generateKey( + { + name: 'ECDH', + namedCurve: curve + }, + true, + ['deriveBits'] + ).then((pair) => { + // forcePrivate is used for testing only + const genSharedKey = (theirPub, forcePrivate, cb) => { + if (typeof forcePrivate === 'function') { + cb = forcePrivate + forcePrivate = undefined + } + + let privateKey + + if (forcePrivate) { + privateKey = crypto.subtle.importKey( + 'jwk', + unmarshalPrivateKey(curve, forcePrivate), + { + name: 'ECDH', + namedCurve: curve + }, + false, + ['deriveBits'] + ) + } else { + privateKey = Promise.resolve(pair.privateKey) + } + + const keys = Promise.all([ + crypto.subtle.importKey( + 'jwk', + unmarshalPublicKey(curve, theirPub), + { + name: 'ECDH', + namedCurve: curve + }, + false, + [] + ), + privateKey + ]) + + nodeify(keys.then((keys) => crypto.subtle.deriveBits( + { + name: 'ECDH', + namedCurve: curve, + public: keys[0] + }, + keys[1], + bits[curve] + )).then((bits) => Buffer.from(bits)), cb) + } + + return crypto.subtle.exportKey( + 'jwk', + pair.publicKey + ).then((publicKey) => { + return { + key: marshalPublicKey(publicKey), + genSharedKey + } + }) + }), callback) +} + +const curveLengths = { + 'P-256': 32, + 'P-384': 48, + 'P-521': 66 +} + +// Marshal converts a jwk encodec ECDH public key into the +// form specified in section 4.3.6 of ANSI X9.62. (This is the format +// go-ipfs uses) +function marshalPublicKey (jwk) { + const byteLen = curveLengths[jwk.crv] + + return Buffer.concat([ + Buffer([4]), // uncompressed point + toBn(jwk.x).toBuffer('be', byteLen), + toBn(jwk.y).toBuffer('be', byteLen) + ], 1 + byteLen * 2) +} + +// Unmarshal converts a point, serialized by Marshal, into an jwk encoded key +function unmarshalPublicKey (curve, key) { + const byteLen = curveLengths[curve] + + if (!key.slice(0, 1).equals(Buffer([4]))) { + throw new Error('Invalid key format') + } + const x = new BN(key.slice(1, byteLen + 1)) + const y = new BN(key.slice(1 + byteLen)) + + return { + kty: 'EC', + crv: curve, + x: toBase64(x), + y: toBase64(y), + ext: true + } +} + +function unmarshalPrivateKey (curve, key) { + const result = unmarshalPublicKey(curve, key.public) + result.d = toBase64(new BN(key.private)) + return result +} diff --git a/src/crypto/ecdh.js b/src/crypto/ecdh.js index 48ca3e9..5a779ed 100644 --- a/src/crypto/ecdh.js +++ b/src/crypto/ecdh.js @@ -1,101 +1,41 @@ 'use strict' -const crypto = require('./webcrypto')() -const nodeify = require('nodeify') -const BN = require('asn1.js').bignum +const crypto = require('crypto') +const setImmediate = require('async/setImmediate') -const util = require('./util') -const toBase64 = util.toBase64 -const toBn = util.toBn +const curves = { + 'P-256': 'prime256v1', + 'P-384': 'secp384r1', + 'P-521': 'secp521r1' +} exports.generateEphmeralKeyPair = function (curve, callback) { - nodeify(crypto.subtle.generateKey( - { - name: 'ECDH', - namedCurve: curve - }, - true, - ['deriveBits'] - ).then((pair) => { - // forcePrivate is used for testing only - const genSharedKey = (theirPub, forcePrivate, cb) => { + if (!curves[curve]) { + return callback(new Error(`Unkown curve: ${curve}`)) + } + const ecdh = crypto.createECDH(curves[curve]) + ecdh.generateKeys() + + setImmediate(() => callback(null, { + key: ecdh.getPublicKey(), + genSharedKey (theirPub, forcePrivate, cb) { if (typeof forcePrivate === 'function') { cb = forcePrivate - forcePrivate = undefined + forcePrivate = null } - const privateKey = forcePrivate || pair.privateKey - nodeify(crypto.subtle.importKey( - 'jwk', - unmarshalPublicKey(curve, theirPub), - { - name: 'ECDH', - namedCurve: curve - }, - false, - [] - ).then((publicKey) => { - return crypto.subtle.deriveBits( - { - name: 'ECDH', - namedCurve: curve, - public: publicKey - }, - privateKey, - 256 - ) - }).then((bits) => { - // return p.derive(pub.getPublic()).toBuffer('be') - return Buffer.from(bits) - }), cb) + if (forcePrivate) { + ecdh.setPrivateKey(forcePrivate.private) + } + + let secret + try { + secret = ecdh.computeSecret(theirPub) + } catch (err) { + return cb(err) + } + + setImmediate(() => cb(null, secret)) } - - return crypto.subtle.exportKey( - 'jwk', - pair.publicKey - ).then((publicKey) => { - return { - key: marshalPublicKey(publicKey), - genSharedKey - } - }) - }), callback) -} - -const curveLengths = { - 'P-256': 32, - 'P-384': 48, - 'P-521': 66 -} - -// Marshal converts a jwk encodec ECDH public key into the -// form specified in section 4.3.6 of ANSI X9.62. (This is the format -// go-ipfs uses) -function marshalPublicKey (jwk) { - const byteLen = curveLengths[jwk.crv] - - return Buffer.concat([ - Buffer([4]), // uncompressed point - toBn(jwk.x).toBuffer('be', byteLen), - toBn(jwk.y).toBuffer('be', byteLen) - ], 1 + byteLen * 2) -} - -// Unmarshal converts a point, serialized by Marshal, into an jwk encoded key -function unmarshalPublicKey (curve, key) { - const byteLen = curveLengths[curve] - - if (!key.slice(0, 1).equals(Buffer([4]))) { - throw new Error('Invalid key format') - } - const x = new BN(key.slice(1, byteLen + 1)) - const y = new BN(key.slice(1 + byteLen)) - - return { - kty: 'EC', - crv: curve, - x: toBase64(x), - y: toBase64(y), - ext: true - } + })) } diff --git a/test/ephemeral-keys.spec.js b/test/ephemeral-keys.spec.js index 38c9214..ff4ed85 100644 --- a/test/ephemeral-keys.spec.js +++ b/test/ephemeral-keys.spec.js @@ -14,6 +14,11 @@ const lengths = { 'P-384': 97, 'P-521': 133 } +const secretLengths = { + 'P-256': 32, + 'P-384': 48, + 'P-521': 66 +} describe('generateEphemeralKeyPair', () => { curves.forEach((curve) => { @@ -28,7 +33,7 @@ describe('generateEphemeralKeyPair', () => { keys[0].genSharedKey(keys[1].key, (err, shared) => { expect(err).to.not.exist - expect(shared).to.have.length(32) + expect(shared).to.have.length(secretLengths[curve]) done() }) }) @@ -39,12 +44,29 @@ describe('generateEphemeralKeyPair', () => { it('generates a shared secret', (done) => { const curve = fixtures.curve - crypto.generateEphemeralKeyPair(curve, (err, alice) => { + parallel([ + (cb) => crypto.generateEphemeralKeyPair(curve, cb), + (cb) => crypto.generateEphemeralKeyPair(curve, cb) + ], (err, res) => { expect(err).to.not.exist + const alice = res[0] + const bob = res[1] + bob.key = fixtures.bob.public - alice.genSharedKey(fixtures.bob.public, (err, s1) => { + parallel([ + (cb) => alice.genSharedKey(bob.key, cb), + (cb) => bob.genSharedKey(alice.key, fixtures.bob, cb) + ], (err, secrets) => { expect(err).to.not.exist - expect(s1).to.have.length(32) + + expect( + secrets[0] + ).to.be.eql( + secrets[1] + ) + + expect(secrets[0]).to.have.length(32) + done() }) }) diff --git a/test/fixtures/go-elliptic-key.js b/test/fixtures/go-elliptic-key.js index fb9824f..75abac5 100644 --- a/test/fixtures/go-elliptic-key.js +++ b/test/fixtures/go-elliptic-key.js @@ -3,9 +3,9 @@ module.exports = { curve: 'P-256', bob: { - private: [ + private: new Buffer([ 181, 217, 162, 151, 225, 36, 53, 253, 107, 66, 27, 27, 232, 72, 0, 0, 103, 167, 84, 62, 203, 91, 97, 137, 131, 193, 230, 126, 98, 242, 216, 170 - ], + ]), public: new Buffer([ 4, 53, 59, 128, 56, 162, 250, 72, 141, 206, 117, 232, 57, 96, 39, 39, 247, 7, 27, 57, 251, 232, 120, 186, 21, 239, 176, 139, 195, 129, 125, 85, 11, 188, 191, 32, 227, 0, 6, 163, 101, 68, 208, 1, 43, 131, 124, 112, 102, 91, 104, 79, 16, 119, 152, 208, 4, 147, 155, 83, 20, 146, 104, 55, 90 ])