From 70cf2d60ff4d709a9765e4be60202fbf8c96a190 Mon Sep 17 00:00:00 2001 From: Ilya Chesnokov Date: Tue, 9 Jul 2019 20:45:28 +0200 Subject: [PATCH] Implement ECDH using Node crypto (#921) --- src/crypto/public_key/elliptic/ecdh.js | 85 +++++++++++++++++++------- test/crypto/elliptic.js | 71 +++++++++++---------- 2 files changed, 103 insertions(+), 53 deletions(-) diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 20bce59c..e129b5b8 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -40,6 +40,7 @@ import enums from '../../../enums'; import util from '../../../util'; const webCrypto = util.getWebCrypto(); +const nodeCrypto = util.getNodeCrypto(); // Build Param for ECDH algorithm (RFC 6637) function buildEcdhParam(public_algo, oid, cipher_algo, hash_algo, fingerprint) { @@ -81,8 +82,8 @@ async function kdf(hash_algo, X, length, param, stripLeading=false, stripTrailin * Generate ECDHE ephemeral key and secret from public key * * @param {Curve} curve Elliptic curve object - * @param {Uint8Array} Q Recipient public key - * @returns {Promise<{V: Uint8Array, S: BN}>} Returns public part of ephemeral key and generated ephemeral secret + * @param {Uint8Array} Q Recipient public key + * @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>} * @async */ async function genPublicEphemeralKey(curve, Q) { @@ -99,14 +100,16 @@ async function genPublicEphemeralKey(curve, Q) { case 'p521': { if (curve.web && util.getWebCrypto()) { try { - const result = await webPublicEphemeralKey(curve, Q); - return result; + return await webPublicEphemeralKey(curve, Q); } catch (err) { util.print_debug_error(err); } } } } + if (curve.node && nodeCrypto) { + return nodePublicEphemeralKey(curve, Q); + } return ellipticPublicEphemeralKey(curve, Q); } @@ -119,7 +122,7 @@ async function genPublicEphemeralKey(curve, Q) { * @param {module:type/mpi} m Value derived from session key (RFC 6637) * @param {Uint8Array} Q Recipient public key * @param {String} fingerprint Recipient fingerprint - * @returns {Promise<{V: BN, C: BN}>} Returns public part of ephemeral key and encoded session key + * @returns {Promise<{publicKey: Uint8Array, wrappedKey: Uint8Array}>} * @async */ async function encrypt(oid, cipher_algo, hash_algo, m, Q, fingerprint) { @@ -139,7 +142,7 @@ async function encrypt(oid, cipher_algo, hash_algo, m, Q, fingerprint) { * @param {Uint8Array} V Public part of ephemeral key * @param {Uint8Array} Q Recipient public key * @param {Uint8Array} d Recipient private key - * @returns {Promise} Generated ephemeral secret + * @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>} * @async */ async function genPrivateEphemeralKey(curve, V, Q, d) { @@ -159,14 +162,16 @@ async function genPrivateEphemeralKey(curve, V, Q, d) { case 'p521': { if (curve.web && util.getWebCrypto()) { try { - const result = await webPrivateEphemeralKey(curve, V, Q, d); - return result; + return await webPrivateEphemeralKey(curve, V, Q, d); } catch (err) { util.print_debug_error(err); } } } } + if (curve.node && nodeCrypto) { + return nodePrivateEphemeralKey(curve, V, d); + } return ellipticPrivateEphemeralKey(curve, V, d); } @@ -181,7 +186,7 @@ async function genPrivateEphemeralKey(curve, V, Q, d) { * @param {Uint8Array} Q Recipient public key * @param {Uint8Array} d Recipient private key * @param {String} fingerprint Recipient fingerprint - * @returns {Promise} Value derived from session + * @returns {Promise} Value derived from session key * @async */ async function decrypt(oid, cipher_algo, hash_algo, V, C, Q, d, fingerprint) { @@ -205,11 +210,11 @@ async function decrypt(oid, cipher_algo, hash_algo, V, C, Q, d, fingerprint) { /** * Generate ECDHE secret from private key and public part of ephemeral key using webCrypto * - * @param {Curve} curve Elliptic curve object - * @param {Uint8Array} V Public part of ephemeral key - * @param {Uint8Array} Q Recipient public key - * @param {Uint8Array} d Recipient private key - * @returns {Promise} Generated ephemeral secret + * @param {Curve} curve Elliptic curve object + * @param {Uint8Array} V Public part of ephemeral key + * @param {Uint8Array} Q Recipient public key + * @param {Uint8Array} d Recipient private key + * @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>} * @async */ async function webPrivateEphemeralKey(curve, V, Q, d) { @@ -259,8 +264,8 @@ async function webPrivateEphemeralKey(curve, V, Q, d) { * Generate ECDHE ephemeral key and secret from public key using webCrypto * * @param {Curve} curve Elliptic curve object - * @param {Uint8Array} Q Recipient public key - * @returns {Promise<{V: Uint8Array, S: BN}>} Returns public part of ephemeral key and generated ephemeral secret + * @param {Uint8Array} Q Recipient public key + * @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>} * @async */ async function webPublicEphemeralKey(curve, Q) { @@ -304,12 +309,12 @@ async function webPublicEphemeralKey(curve, Q) { } /** - * Generate ECDHE secret from private key and public part of ephemeral key using webCrypto + * Generate ECDHE secret from private key and public part of ephemeral key using indutny/elliptic * * @param {Curve} curve Elliptic curve object * @param {Uint8Array} V Public part of ephemeral key * @param {Uint8Array} d Recipient private key - * @returns {Promise} Generated ephemeral secret + * @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>} * @async */ async function ellipticPrivateEphemeralKey(curve, V, d) { @@ -323,11 +328,11 @@ async function ellipticPrivateEphemeralKey(curve, V, d) { } /** - * Generate ECDHE ephemeral key and secret from public key using indutny/elliptic library + * Generate ECDHE ephemeral key and secret from public key using indutny/elliptic * * @param {Curve} curve Elliptic curve object * @param {Uint8Array} Q Recipient public key - * @returns {Promise<{V: Uint8Array, S: BN}>} Returns public part of ephemeral key and generated ephemeral secret + * @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>} * @async */ async function ellipticPublicEphemeralKey(curve, Q) { @@ -340,10 +345,44 @@ async function ellipticPublicEphemeralKey(curve, Q) { return { publicKey, sharedKey }; } +/** + * Generate ECDHE secret from private key and public part of ephemeral key using nodeCrypto + * + * @param {Curve} curve Elliptic curve object + * @param {Uint8Array} V Public part of ephemeral key + * @param {Uint8Array} d Recipient private key + * @returns {Promise<{secretKey: Uint8Array, sharedKey: Uint8Array}>} + * @async + */ +async function nodePrivateEphemeralKey(curve, V, d) { + const recipient = nodeCrypto.createECDH(curve.node.node); + recipient.setPrivateKey(d); + const sharedKey = new Uint8Array(recipient.computeSecret(V)); + const secretKey = new Uint8Array(recipient.getPrivateKey()); + return { secretKey, sharedKey }; +} + +/** + * Generate ECDHE ephemeral key and secret from public key using nodeCrypto + * + * @param {Curve} curve Elliptic curve object + * @param {Uint8Array} Q Recipient public key + * @returns {Promise<{publicKey: Uint8Array, sharedKey: Uint8Array}>} + * @async + */ +async function nodePublicEphemeralKey(curve, Q) { + const sender = nodeCrypto.createECDH(curve.node.node); + sender.generateKeys(); + const sharedKey = new Uint8Array(sender.computeSecret(Q)); + const publicKey = new Uint8Array(sender.getPublicKey()); + return { publicKey, sharedKey }; +} + /** * @param {Integer} payloadSize ec payload size * @param {String} name curve name * @param {Uint8Array} publicKey public key + * @returns {JsonWebKey} public key in jwk format */ function rawPublicToJwk(payloadSize, name, publicKey) { const len = payloadSize; @@ -364,7 +403,8 @@ function rawPublicToJwk(payloadSize, name, publicKey) { * @param {Integer} payloadSize ec payload size * @param {String} name curve name * @param {Uint8Array} publicKey public key - * @param {Uint8Array} privateKey private key + * @param {Uint8Array} privateKey private key + * @returns {JsonWebKey} private key in jwk format */ function privateToJwk(payloadSize, name, privateKey, publicKey) { const jwk = rawPublicToJwk(payloadSize, name, publicKey); @@ -374,6 +414,7 @@ function privateToJwk(payloadSize, name, privateKey, publicKey) { /** * @param {JsonWebKey} jwk key for conversion + * @returns {Uint8Array} raw public key */ function jwkToRawPublic(jwk) { const bufX = util.b64_to_Uint8Array(jwk.x); @@ -385,4 +426,4 @@ function jwkToRawPublic(jwk) { return publicKey; } -export default { encrypt, decrypt, genPublicEphemeralKey, genPrivateEphemeralKey, buildEcdhParam, kdf, webPublicEphemeralKey, ellipticPublicEphemeralKey, ellipticPrivateEphemeralKey, webPrivateEphemeralKey }; +export default { encrypt, decrypt, genPublicEphemeralKey, genPrivateEphemeralKey, buildEcdhParam, kdf, webPublicEphemeralKey, webPrivateEphemeralKey, ellipticPublicEphemeralKey, ellipticPrivateEphemeralKey, nodePublicEphemeralKey, nodePrivateEphemeralKey }; diff --git a/test/crypto/elliptic.js b/test/crypto/elliptic.js index 3cebf3aa..17aeaa1a 100644 --- a/test/crypto/elliptic.js +++ b/test/crypto/elliptic.js @@ -1,5 +1,4 @@ const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp'); - const chai = require('chai'); chai.use(require('chai-as-promised')); @@ -384,12 +383,12 @@ describe('Elliptic Curve Cryptography', function () { it('Invalid ephemeral key', function (done) { expect(decrypt_message( 'secp256k1', 2, 7, [], [], [], [], [] - )).to.be.rejectedWith(Error, /Unknown point format/).notify(done); + )).to.be.rejectedWith(Error, /Private key is not valid for specified curve|Unknown point format/).notify(done); }); it('Invalid elliptic public key', function (done) { expect(decrypt_message( 'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_invalid_point, secp256k1_data, [] - )).to.be.rejectedWith(Error, /Invalid elliptic public key/).notify(done); + )).to.be.rejectedWith(Error, /Public key is not valid for specified curve|Failed to translate Buffer to a EC_POINT|Invalid elliptic public key/).notify(done); }); it('Invalid key data integrity', function (done) { expect(decrypt_message( @@ -464,30 +463,26 @@ describe('Elliptic Curve Cryptography', function () { return Z; } - async function genWebPrivateEphemeralKey(curve, V, Q, d, fingerprint) { + async function genPrivateEphemeralKeySpecific(fun, curve, V, Q, d, fingerprint) { const curveObj = new openpgp.crypto.publicKey.elliptic.Curve(curve); const oid = new openpgp.OID(curveObj.oid); - const { sharedKey } = await openpgp.crypto.publicKey.elliptic.ecdh.webPrivateEphemeralKey( - curveObj, V, Q, d - ); - let cipher_algo = curveObj.cipher; - const hash_algo = curveObj.hash; - const param = openpgp.crypto.publicKey.elliptic.ecdh.buildEcdhParam( - openpgp.enums.publicKey.ecdh, oid, cipher_algo, hash_algo, fingerprint - ); - cipher_algo = openpgp.enums.read(openpgp.enums.symmetric, cipher_algo); - const Z = await openpgp.crypto.publicKey.elliptic.ecdh.kdf( - hash_algo, sharedKey, openpgp.crypto.cipher[cipher_algo].keySize, param, curveObj, false - ); - return Z; - } - - async function genEllipticPrivateEphemeralKey(curve, V, Q, d, fingerprint) { - const curveObj = new openpgp.crypto.publicKey.elliptic.Curve(curve); - const oid = new openpgp.OID(curveObj.oid); - const { sharedKey } = await openpgp.crypto.publicKey.elliptic.ecdh.ellipticPrivateEphemeralKey( - curveObj, V, d - ); + let result; + switch (fun) { + case 'webPrivateEphemeralKey': { + result = await openpgp.crypto.publicKey.elliptic.ecdh[fun]( + curveObj, V, Q, d + ); + break; + } + case 'nodePrivateEphemeralKey': + case 'ellipticPrivateEphemeralKey': { + result = await openpgp.crypto.publicKey.elliptic.ecdh[fun]( + curveObj, V, d + ); + break; + } + } + const sharedKey = result.sharedKey; let cipher_algo = curveObj.cipher; const hash_algo = curveObj.hash; const param = openpgp.crypto.publicKey.elliptic.ecdh.buildEcdhParam( @@ -503,7 +498,7 @@ describe('Elliptic Curve Cryptography', function () { describe('ECDHE key generation', function () { it('Invalid curve', function (done) { expect(genPublicEphemeralKey("secp256k1", Q1, fingerprint1) - ).to.be.rejectedWith(Error, /Unknown point format/).notify(done); + ).to.be.rejectedWith(Error, /Public key is not valid for specified curve|Failed to translate Buffer to a EC_POINT|Unknown point format/).notify(done); }); it('Invalid public part of ephemeral key and private key', async function () { const ECDHE_VZ1 = await genPublicEphemeralKey("curve25519", Q1, fingerprint1); @@ -541,7 +536,8 @@ describe('Elliptic Curve Cryptography', function () { const ECDHE_Z1 = await genPrivateEphemeralKey("p521", ECDHE_VZ1.V, key_data.p521.pub, key_data.p521.priv, fingerprint1); expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_VZ1.Z).join(' ')).to.be.true; }); - it('Comparing keys derived using different algorithms', async function () { + + it('Comparing keys derived using webCrypto and elliptic', async function () { const names = ["p256", "p384", "p521"]; if (!openpgp.util.getWebCrypto()) { this.skip(); @@ -558,11 +554,24 @@ describe('Elliptic Curve Cryptography', function () { return; } const ECDHE_VZ1 = await genPublicEphemeralKey(name, key_data[name].pub, fingerprint1); - const ECDHE_Z1 = await genEllipticPrivateEphemeralKey(name, ECDHE_VZ1.V, key_data[name].pub, key_data[name].priv, fingerprint1); - const ECDHE_Z2 = await genWebPrivateEphemeralKey(name, ECDHE_VZ1.V, key_data[name].pub, key_data[name].priv, fingerprint1); + const ECDHE_Z1 = await genPrivateEphemeralKeySpecific('ellipticPrivateEphemeralKey', name, ECDHE_VZ1.V, key_data[name].pub, key_data[name].priv, fingerprint1); + const ECDHE_Z2 = await genPrivateEphemeralKeySpecific('webPrivateEphemeralKey', name, ECDHE_VZ1.V, key_data[name].pub, key_data[name].priv, fingerprint1); expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_VZ1.Z).join(' ')).to.be.true; - expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_Z2).join(' ')).to.be.true; + expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_Z2).join(' ')).to.be.true; })); }); - }); + it('Comparing keys derived using nodeCrypto and elliptic', async function () { + const names = ["p256", "p384", "p521"]; + if (!openpgp.util.getNodeCrypto()) { + this.skip(); + } + return Promise.all(names.map(async function (name) { + const ECDHE_VZ1 = await genPublicEphemeralKey(name, key_data[name].pub, fingerprint1); + const ECDHE_Z1 = await genPrivateEphemeralKeySpecific('ellipticPrivateEphemeralKey', name, ECDHE_VZ1.V, key_data[name].pub, key_data[name].priv, fingerprint1); + const ECDHE_Z2 = await genPrivateEphemeralKeySpecific('nodePrivateEphemeralKey', name, ECDHE_VZ1.V, key_data[name].pub, key_data[name].priv, fingerprint1); + expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_VZ1.Z).join(' ')).to.be.true; + expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_Z2).join(' ')).to.be.true; + })); + }); + }); });