Implement ECDH using Web Crypto for supported (NIST) curves (#914)

This commit is contained in:
chesnokovilya 2019-06-27 19:21:32 +02:00 committed by Daniel Huigens
parent 32b4f2bd27
commit 1bd5689d75
4 changed files with 337 additions and 52 deletions

View File

@ -138,9 +138,10 @@ export default {
const kdf_params = key_params[2];
const V = data_params[0].toUint8Array();
const C = data_params[1].data;
const Q = key_params[1].toUint8Array();
const d = key_params[3].toUint8Array();
return publicKey.elliptic.ecdh.decrypt(
oid, kdf_params.cipher, kdf_params.hash, V, C, d, fingerprint);
oid, kdf_params.cipher, kdf_params.hash, V, C, Q, d, fingerprint);
}
default:
throw new Error('Invalid public key encryption algorithm.');

View File

@ -64,7 +64,8 @@ const curves = {
cipher: enums.symmetric.aes128,
node: nodeCurves.p256,
web: webCurves.p256,
payloadSize: 32
payloadSize: 32,
sharedSize: 256
},
p384: {
oid: [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x22],
@ -73,7 +74,8 @@ const curves = {
cipher: enums.symmetric.aes192,
node: nodeCurves.p384,
web: webCurves.p384,
payloadSize: 48
payloadSize: 48,
sharedSize: 384
},
p521: {
oid: [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x23],
@ -82,7 +84,8 @@ const curves = {
cipher: enums.symmetric.aes256,
node: nodeCurves.p521,
web: webCurves.p521,
payloadSize: 66
payloadSize: 66,
sharedSize: 528
},
secp256k1: {
oid: [0x06, 0x05, 0x2B, 0x81, 0x04, 0x00, 0x0A],

View File

@ -39,6 +39,8 @@ import type_kdf_params from '../../../type/kdf_params';
import enums from '../../../enums';
import util from '../../../util';
const webCrypto = util.getWebCrypto();
// Build Param for ECDH algorithm (RFC 6637)
function buildEcdhParam(public_algo, oid, cipher_algo, hash_algo, fingerprint) {
const kdf_params = new type_kdf_params([hash_algo, cipher_algo]);
@ -84,20 +86,28 @@ async function kdf(hash_algo, X, length, param, stripLeading=false, stripTrailin
* @async
*/
async function genPublicEphemeralKey(curve, Q) {
if (curve.name === 'curve25519') {
const { secretKey: d } = nacl.box.keyPair();
const { secretKey, sharedKey } = await genPrivateEphemeralKey(curve, Q, d);
let { publicKey } = nacl.box.keyPair.fromSecretKey(secretKey);
publicKey = util.concatUint8Array([new Uint8Array([0x40]), publicKey]);
return { publicKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below
switch (curve.name) {
case 'curve25519': {
const { secretKey: d } = nacl.box.keyPair();
const { secretKey, sharedKey } = await genPrivateEphemeralKey(curve, Q, null, d);
let { publicKey } = nacl.box.keyPair.fromSecretKey(secretKey);
publicKey = util.concatUint8Array([new Uint8Array([0x40]), publicKey]);
return { publicKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below
}
case 'p256':
case 'p384':
case 'p521': {
if (curve.web && util.getWebCrypto()) {
try {
const result = await webPublicEphemeralKey(curve, Q);
return result;
} catch (err) {
util.print_debug_error(err);
}
}
}
}
const v = await curve.genKeyPair();
Q = curve.keyFromPublic(Q);
const publicKey = new Uint8Array(v.getPublic());
const S = v.derive(Q);
const len = curve.curve.curve.p.byteLength();
const sharedKey = S.toArrayLike(Uint8Array, 'be', len);
return { publicKey, sharedKey };
return ellipticPublicEphemeralKey(curve, Q);
}
/**
@ -127,28 +137,37 @@ async function encrypt(oid, cipher_algo, hash_algo, m, Q, fingerprint) {
*
* @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<BN>} Generated ephemeral secret
* @async
*/
async function genPrivateEphemeralKey(curve, V, d) {
if (curve.name === 'curve25519') {
const one = new BN(1);
const mask = one.ushln(255 - 3).sub(one).ushln(3);
let secretKey = new BN(d);
secretKey = secretKey.or(one.ushln(255 - 1));
secretKey = secretKey.and(mask);
secretKey = secretKey.toArrayLike(Uint8Array, 'le', 32);
const sharedKey = nacl.scalarMult(secretKey, V.subarray(1));
return { secretKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below
async function genPrivateEphemeralKey(curve, V, Q, d) {
switch (curve.name) {
case 'curve25519': {
const one = new BN(1);
const mask = one.ushln(255 - 3).sub(one).ushln(3);
let secretKey = new BN(d);
secretKey = secretKey.or(one.ushln(255 - 1));
secretKey = secretKey.and(mask);
secretKey = secretKey.toArrayLike(Uint8Array, 'le', 32);
const sharedKey = nacl.scalarMult(secretKey, V.subarray(1));
return { secretKey, sharedKey }; // Note: sharedKey is little-endian here, unlike below
}
case 'p256':
case 'p384':
case 'p521': {
if (curve.web && util.getWebCrypto()) {
try {
const result = await webPrivateEphemeralKey(curve, V, Q, d);
return result;
} catch (err) {
util.print_debug_error(err);
}
}
}
}
V = curve.keyFromPublic(V);
d = curve.keyFromPrivate(d);
const secretKey = new Uint8Array(d.getPrivate());
const S = d.derive(V);
const len = curve.curve.curve.p.byteLength();
const sharedKey = S.toArrayLike(Uint8Array, 'be', len);
return { secretKey, sharedKey };
return ellipticPrivateEphemeralKey(curve, V, d);
}
/**
@ -159,14 +178,15 @@ async function genPrivateEphemeralKey(curve, V, d) {
* @param {module:enums.hash} hash_algo Hash algorithm to use
* @param {Uint8Array} V Public part of ephemeral key
* @param {Uint8Array} C Encrypted and wrapped value derived from session key
* @param {Uint8Array} Q Recipient public key
* @param {Uint8Array} d Recipient private key
* @param {String} fingerprint Recipient fingerprint
* @returns {Promise<BN>} Value derived from session
* @async
*/
async function decrypt(oid, cipher_algo, hash_algo, V, C, d, fingerprint) {
async function decrypt(oid, cipher_algo, hash_algo, V, C, Q, d, fingerprint) {
const curve = new Curve(oid);
const { sharedKey } = await genPrivateEphemeralKey(curve, V, d);
const { sharedKey } = await genPrivateEphemeralKey(curve, V, Q, d);
const param = buildEcdhParam(enums.publicKey.ecdh, oid, cipher_algo, hash_algo, fingerprint);
cipher_algo = enums.read(enums.symmetric, cipher_algo);
let err;
@ -182,4 +202,187 @@ async function decrypt(oid, cipher_algo, hash_algo, V, C, d, fingerprint) {
throw err;
}
export default { encrypt, decrypt, genPublicEphemeralKey, genPrivateEphemeralKey, buildEcdhParam, kdf };
/**
* 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<BN>} Generated ephemeral secret
* @async
*/
async function webPrivateEphemeralKey(curve, V, Q, d) {
const recipient = privateToJwk(curve.payloadSize, curve.web.web, d, Q);
let privateKey = webCrypto.importKey(
"jwk",
recipient,
{
name: "ECDH",
namedCurve: curve.web.web
},
true,
["deriveKey", "deriveBits"]
);
const jwk = rawPublicToJwk(curve.payloadSize, curve.web.web, V);
let sender = webCrypto.importKey(
"jwk",
jwk,
{
name: "ECDH",
namedCurve: curve.web.web
},
true,
[]
);
[privateKey, sender] = await Promise.all([privateKey, sender]);
let S = webCrypto.deriveBits(
{
name: "ECDH",
namedCurve: curve.web.web,
public: sender
},
privateKey,
curve.web.sharedSize
);
let secret = webCrypto.exportKey(
"jwk",
privateKey
);
[S, secret] = await Promise.all([S, secret]);
const sharedKey = new Uint8Array(S);
const secretKey = util.b64_to_Uint8Array(secret.d, true);
return { secretKey, sharedKey };
}
/**
* 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
* @async
*/
async function webPublicEphemeralKey(curve, Q) {
const jwk = rawPublicToJwk(curve.payloadSize, curve.web.web, Q);
let keyPair = webCrypto.generateKey(
{
name: "ECDH",
namedCurve: curve.web.web
},
true,
["deriveKey", "deriveBits"]
);
let recipient = webCrypto.importKey(
"jwk",
jwk,
{
name: "ECDH",
namedCurve: curve.web.web
},
false,
[]
);
[keyPair, recipient] = await Promise.all([keyPair, recipient]);
let s = webCrypto.deriveBits(
{
name: "ECDH",
namedCurve: curve.web.web,
public: recipient
},
keyPair.privateKey,
curve.web.sharedSize
);
let p = webCrypto.exportKey(
"jwk",
keyPair.publicKey
);
[s, p] = await Promise.all([s, p]);
const sharedKey = new Uint8Array(s);
const publicKey = new Uint8Array(jwkToRawPublic(p));
return { publicKey, sharedKey };
}
/**
* 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} d Recipient private key
* @returns {Promise<BN>} Generated ephemeral secret
* @async
*/
async function ellipticPrivateEphemeralKey(curve, V, d) {
V = curve.keyFromPublic(V);
d = curve.keyFromPrivate(d);
const secretKey = new Uint8Array(d.getPrivate());
const S = d.derive(V);
const len = curve.curve.curve.p.byteLength();
const sharedKey = S.toArrayLike(Uint8Array, 'be', len);
return { secretKey, sharedKey };
}
/**
* Generate ECDHE ephemeral key and secret from public key using indutny/elliptic library
*
* @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
* @async
*/
async function ellipticPublicEphemeralKey(curve, Q) {
const v = await curve.genKeyPair();
Q = curve.keyFromPublic(Q);
const publicKey = new Uint8Array(v.getPublic());
const S = v.derive(Q);
const len = curve.curve.curve.p.byteLength();
const sharedKey = S.toArrayLike(Uint8Array, 'be', len);
return { publicKey, sharedKey };
}
/**
* @param {Integer} payloadSize ec payload size
* @param {String} name curve name
* @param {Uint8Array} publicKey public key
*/
function rawPublicToJwk(payloadSize, name, publicKey) {
const len = payloadSize;
const bufX = publicKey.slice(1, len+1);
const bufY = publicKey.slice(len+1, len*2+1);
// https://www.rfc-editor.org/rfc/rfc7518.txt
const jwKey = {
kty: "EC",
crv: name,
x: util.Uint8Array_to_b64(bufX, true),
y: util.Uint8Array_to_b64(bufY, true),
ext: true
};
return jwKey;
}
/**
* @param {Integer} payloadSize ec payload size
* @param {String} name curve name
* @param {Uint8Array} publicKey public key
* @param {Uint8Array} privateKey private key
*/
function privateToJwk(payloadSize, name, privateKey, publicKey) {
const jwk = rawPublicToJwk(payloadSize, name, publicKey);
jwk.d = util.Uint8Array_to_b64(privateKey, true);
return jwk;
}
/**
* @param {JsonWebKey} jwk key for conversion
*/
function jwkToRawPublic(jwk) {
const bufX = util.b64_to_Uint8Array(jwk.x);
const bufY = util.b64_to_Uint8Array(jwk.y);
const publicKey = new Uint8Array(bufX.length + bufY.length + 1);
publicKey[0] = 0x04;
publicKey.set(bufX, 1);
publicKey.set(bufY, bufX.length+1);
return publicKey;
}
export default { encrypt, decrypt, genPublicEphemeralKey, genPrivateEphemeralKey, buildEcdhParam, kdf, webPublicEphemeralKey, ellipticPublicEphemeralKey, ellipticPrivateEphemeralKey, webPrivateEphemeralKey };

View File

@ -1,7 +1,7 @@
const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp');
const BN = require('bn.js');
const chai = require('chai');
chai.use(require('chai-as-promised'));
const expect = chai.expect;
@ -25,7 +25,8 @@ describe('Elliptic Curve Cryptography', function () {
0x6F, 0xAA, 0xE7, 0xFD, 0xC4, 0x7D, 0x89, 0xCC,
0x06, 0xCA, 0xFE, 0xAE, 0xCD, 0x0E, 0x9E, 0x62,
0x57, 0xA4, 0xC3, 0xE7, 0x5E, 0x69, 0x10, 0xEE,
0x67, 0xC2, 0x09, 0xF9, 0xEF, 0xE7, 0x9E, 0x56])
0x67, 0xC2, 0x09, 0xF9, 0xEF, 0xE7, 0x9E, 0x56
])
},
p384: {
priv: new Uint8Array([
@ -324,7 +325,7 @@ describe('Elliptic Curve Cryptography', function () {
});
});
describe('ECDH key exchange', function () {
const decrypt_message = function (oid, hash, cipher, priv, ephemeral, data, fingerprint) {
const decrypt_message = function (oid, hash, cipher, priv, pub, ephemeral, data, fingerprint) {
if (openpgp.util.isString(data)) {
data = openpgp.util.str_to_Uint8Array(data);
} else {
@ -338,6 +339,7 @@ describe('Elliptic Curve Cryptography', function () {
hash,
new Uint8Array(ephemeral),
data,
new Uint8Array(pub),
new Uint8Array(priv),
new Uint8Array(fingerprint)
);
@ -376,26 +378,26 @@ describe('Elliptic Curve Cryptography', function () {
it('Invalid curve oid', function (done) {
expect(decrypt_message(
'', 2, 7, [], [], [], []
'', 2, 7, [], [], [], [], []
)).to.be.rejectedWith(Error, /Not valid curve/).notify(done);
});
it('Invalid ephemeral key', function (done) {
expect(decrypt_message(
'secp256k1', 2, 7, [], [], [], []
'secp256k1', 2, 7, [], [], [], [], []
)).to.be.rejectedWith(Error, /Unknown point format/).notify(done);
});
it('Invalid elliptic public key', function (done) {
expect(decrypt_message(
'secp256k1', 2, 7, secp256k1_value, secp256k1_invalid_point, secp256k1_data, []
'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_invalid_point, secp256k1_data, []
)).to.be.rejectedWith(Error, /Invalid elliptic public key/).notify(done);
});
it('Invalid key data integrity', function (done) {
expect(decrypt_message(
'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_data, []
'secp256k1', 2, 7, secp256k1_value, secp256k1_point, secp256k1_point, secp256k1_data, []
)).to.be.rejectedWith(Error, /Key Data Integrity failed/).notify(done);
});
});
const Q1 = new Uint8Array([
64,
48, 226, 162, 114, 194, 194, 67, 214,
@ -443,10 +445,47 @@ describe('Elliptic Curve Cryptography', function () {
);
return { V, Z };
}
async function genPrivateEphemeralKey(curve, V, d, fingerprint) {
async function genPrivateEphemeralKey(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.genPrivateEphemeralKey(
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 genWebPrivateEphemeralKey(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 cipher_algo = curveObj.cipher;
@ -460,6 +499,7 @@ describe('Elliptic Curve Cryptography', function () {
);
return Z;
}
describe('ECDHE key generation', function () {
it('Invalid curve', function (done) {
expect(genPublicEphemeralKey("secp256k1", Q1, fingerprint1)
@ -467,24 +507,62 @@ describe('Elliptic Curve Cryptography', function () {
});
it('Invalid public part of ephemeral key and private key', async function () {
const ECDHE_VZ1 = await genPublicEphemeralKey("curve25519", Q1, fingerprint1);
const ECDHE_Z12 = await genPrivateEphemeralKey("curve25519", ECDHE_VZ1.V, d2, fingerprint1);
const ECDHE_Z12 = await genPrivateEphemeralKey("curve25519", ECDHE_VZ1.V, Q2, d2, fingerprint1);
expect(Array.from(ECDHE_Z12).join(' ') === Array.from(ECDHE_VZ1.Z).join(' ')).to.be.false;
});
it('Invalid fingerprint', async function () {
const ECDHE_VZ2 = await genPublicEphemeralKey("curve25519", Q2, fingerprint1);
const ECDHE_Z2 = await genPrivateEphemeralKey("curve25519", ECDHE_VZ2.V, d2, fingerprint2);
const ECDHE_Z2 = await genPrivateEphemeralKey("curve25519", ECDHE_VZ2.V, Q2, d2, fingerprint2);
expect(Array.from(ECDHE_Z2).join(' ') === Array.from(ECDHE_VZ2.Z).join(' ')).to.be.false;
});
it('Different keys', async function () {
const ECDHE_VZ1 = await genPublicEphemeralKey("curve25519", Q1, fingerprint1);
const ECDHE_VZ2 = await genPublicEphemeralKey("curve25519", Q2, fingerprint1);
const ECDHE_Z1 = await genPrivateEphemeralKey("curve25519", ECDHE_VZ1.V, d1, fingerprint1);
const ECDHE_Z1 = await genPrivateEphemeralKey("curve25519", ECDHE_VZ1.V, Q1, d1, fingerprint1);
expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_VZ2.Z).join(' ')).to.be.false;
});
it('Successful exchange', async function () {
it('Successful exchange curve25519', async function () {
const ECDHE_VZ1 = await genPublicEphemeralKey("curve25519", Q1, fingerprint1);
const ECDHE_Z1 = await genPrivateEphemeralKey("curve25519", ECDHE_VZ1.V, d1, fingerprint1);
const ECDHE_Z1 = await genPrivateEphemeralKey("curve25519", ECDHE_VZ1.V, Q1, d1, fingerprint1);
expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_VZ1.Z).join(' ')).to.be.true;
});
});
it('Successful exchange NIST P256', async function () {
const ECDHE_VZ1 = await genPublicEphemeralKey("p256", key_data.p256.pub, fingerprint1);
const ECDHE_Z1 = await genPrivateEphemeralKey("p256", ECDHE_VZ1.V, key_data.p256.pub, key_data.p256.priv, fingerprint1);
expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_VZ1.Z).join(' ')).to.be.true;
});
it('Successful exchange NIST P384', async function () {
const ECDHE_VZ1 = await genPublicEphemeralKey("p384", key_data.p384.pub, fingerprint1);
const ECDHE_Z1 = await genPrivateEphemeralKey("p384", ECDHE_VZ1.V, key_data.p384.pub, key_data.p384.priv, fingerprint1);
expect(Array.from(ECDHE_Z1).join(' ') === Array.from(ECDHE_VZ1.Z).join(' ')).to.be.true;
});
it('Successful exchange NIST P521', async function () {
const ECDHE_VZ1 = await genPublicEphemeralKey("p521", key_data.p521.pub, fingerprint1);
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 () {
const names = ["p256", "p384", "p521"];
if (!openpgp.util.getWebCrypto()) {
this.skip();
}
return Promise.all(names.map(async function (name) {
const curve = new elliptic_curves.Curve(name);
try {
await window.crypto.subtle.generateKey({
name: "ECDSA",
namedCurve: curve.web.web
}, false, ["sign", "verify"]);
} catch(err) {
openpgp.util.print_debug_error(err);
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);
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;
}));
});
});
});