diff --git a/src/crypto/hkdf.js b/src/crypto/hkdf.js index 361703b1..69a85ae7 100644 --- a/src/crypto/hkdf.js +++ b/src/crypto/hkdf.js @@ -6,9 +6,8 @@ import enums from '../enums'; import util from '../util'; -const webCrypto = util.getWebCrypto(); - export default async function computeHKDF(hashAlgo, inputKey, salt, info, outLen) { + const webCrypto = util.getWebCrypto(); const hash = enums.read(enums.webHash, hashAlgo); if (!hash) throw new Error('Hash algo not supported with HKDF'); diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 66937b2c..4b3d6606 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -30,9 +30,6 @@ import * as pkcs5 from '../../pkcs5'; import { getCipherParams } from '../../cipher'; import { generateEphemeralEncryptionMaterial as ecdhXGenerateEphemeralEncryptionMaterial, recomputeSharedSecret as ecdhXRecomputeSharedSecret } from './ecdh_x'; -const webCrypto = util.getWebCrypto(); -const nodeCrypto = util.getNodeCrypto(); - /** * Validate ECDH parameters * @param {module:type/oid} oid - Elliptic curve object identifier @@ -238,6 +235,7 @@ async function jsPublicEphemeralKey(curve, Q) { * @async */ async function webPrivateEphemeralKey(curve, V, Q, d) { + const webCrypto = util.getWebCrypto(); const recipient = privateToJWK(curve.payloadSize, curve.web, Q, d); let privateKey = webCrypto.importKey( 'jwk', @@ -289,6 +287,7 @@ async function webPrivateEphemeralKey(curve, V, Q, d) { * @async */ async function webPublicEphemeralKey(curve, Q) { + const webCrypto = util.getWebCrypto(); const jwk = rawPublicToJWK(curve.payloadSize, curve.web, Q); let keyPair = webCrypto.generateKey( { @@ -338,6 +337,7 @@ async function webPublicEphemeralKey(curve, Q) { * @async */ async function nodePrivateEphemeralKey(curve, V, d) { + const nodeCrypto = util.getNodeCrypto(); const recipient = nodeCrypto.createECDH(curve.node); recipient.setPrivateKey(d); const sharedKey = new Uint8Array(recipient.computeSecret(V)); @@ -354,6 +354,7 @@ async function nodePrivateEphemeralKey(curve, V, d) { * @async */ async function nodePublicEphemeralKey(curve, Q) { + const nodeCrypto = util.getNodeCrypto(); const sender = nodeCrypto.createECDH(curve.node); sender.generateKeys(); const sharedKey = new Uint8Array(sender.computeSecret(Q)); diff --git a/src/crypto/public_key/elliptic/ecdh_x.js b/src/crypto/public_key/elliptic/ecdh_x.js index 1dac58b9..ba959380 100644 --- a/src/crypto/public_key/elliptic/ecdh_x.js +++ b/src/crypto/public_key/elliptic/ecdh_x.js @@ -3,9 +3,7 @@ * @module crypto/public_key/elliptic/ecdh */ -import x25519 from '@openpgp/tweetnacl'; import * as aesKW from '../../aes_kw'; -import { getRandomBytes } from '../../random'; import enums from '../../../enums'; import util from '../../../util'; @@ -55,9 +53,9 @@ export async function generate(algo) { if (err.name !== 'NotSupportedError') { throw err; } + const { default: x25519 } = await import('@openpgp/tweetnacl'); // k stays in little-endian, unlike legacy ECDH over curve25519 - const k = getRandomBytes(32); - const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k); + const { secretKey: k, publicKey: A } = x25519.box.keyPair(); return { A, k }; } @@ -240,10 +238,10 @@ export async function generateEphemeralEncryptionMaterial(algo, recipientA) { if (err.name !== 'NotSupportedError') { throw err; } - const ephemeralSecretKey = getRandomBytes(getPayloadSize(algo)); + const { default: x25519 } = await import('@openpgp/tweetnacl'); + const { secretKey: ephemeralSecretKey, publicKey: ephemeralPublicKey } = x25519.box.keyPair(); const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA); assertNonZeroArray(sharedSecret); - const { publicKey: ephemeralPublicKey } = x25519.box.keyPair.fromSecretKey(ephemeralSecretKey); return { ephemeralPublicKey, sharedSecret }; } case enums.publicKey.x448: { @@ -278,6 +276,7 @@ export async function recomputeSharedSecret(algo, ephemeralPublicKey, A, k) { if (err.name !== 'NotSupportedError') { throw err; } + const { default: x25519 } = await import('@openpgp/tweetnacl'); const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey); assertNonZeroArray(sharedSecret); return sharedSecret; diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index 7afdde85..d94924b8 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -20,10 +20,9 @@ * @module crypto/public_key/elliptic/eddsa */ -import ed25519 from '@openpgp/tweetnacl'; import util from '../../../util'; import enums from '../../../enums'; -import { computeDigest, getHashByteLength } from '../../hash'; +import { getHashByteLength } from '../../hash'; import { getRandomBytes } from '../../random'; import { b64ToUint8Array, uint8ArrayToB64 } from '../../../encoding/base64'; @@ -59,7 +58,9 @@ export async function generate(algo) { if (err.name !== 'NotSupportedError') { throw err; } + const { default: ed25519 } = await import('@openpgp/tweetnacl'); const seed = getRandomBytes(getPayloadSize(algo)); + // not using `ed25519.sign.keyPair` since it returns the expanded secret, so using `fromSeed` instead is more straightforward const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed); return { A, seed }; } @@ -111,6 +112,7 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe if (err.name !== 'NotSupportedError') { throw err; } + const { default: ed25519 } = await import('@openpgp/tweetnacl'); const secretKey = util.concatUint8Array([privateKey, publicKey]); const signature = ed25519.sign.detached(hashed, secretKey); return { RS: signature }; @@ -157,6 +159,7 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { if (err.name !== 'NotSupportedError') { throw err; } + const { default: ed25519 } = await import('@openpgp/tweetnacl'); return ed25519.sign.detached.verify(hashed, RS, publicKey); } @@ -203,7 +206,7 @@ export async function validateParams(algo, A, seed) { if (err.name !== 'NotSupportedError') { return false; } - + const { default: ed25519 } = await import('@openpgp/tweetnacl'); const { publicKey } = ed25519.sign.keyPair.fromSeed(seed); return util.equalsUint8Array(A, publicKey); } diff --git a/src/crypto/public_key/elliptic/oid_curves.js b/src/crypto/public_key/elliptic/oid_curves.js index 975dcd8e..38edb609 100644 --- a/src/crypto/public_key/elliptic/oid_curves.js +++ b/src/crypto/public_key/elliptic/oid_curves.js @@ -19,7 +19,6 @@ * @fileoverview Wrapper of an instance of an Elliptic Curve * @module crypto/public_key/elliptic/curve */ -import nacl from '@openpgp/tweetnacl'; import enums from '../../../enums'; import util from '../../../util'; import { uint8ArrayToB64, b64ToUint8Array } from '../../../encoding/base64'; diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index 0e73cdcb..de17e9a8 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -323,7 +323,10 @@ export default () => describe('ECDH key exchange @lightweight', function () { const disableNative = () => { enableNative(); // stubbed functions return undefined - getWebCryptoStub = sinonSandbox.stub(util, 'getWebCrypto'); + getWebCryptoStub = sinonSandbox.stub(util, 'getWebCrypto').returns({ + generateKey: () => { const e = new Error('getWebCrypto is mocked'); e.name = 'NotSupportedError'; throw e; }, + importKey: () => { const e = new Error('getWebCrypto is mocked'); e.name = 'NotSupportedError'; throw e; } + }); getNodeCryptoStub = sinonSandbox.stub(util, 'getNodeCrypto'); }; const enableNative = () => { @@ -331,6 +334,46 @@ export default () => describe('ECDH key exchange @lightweight', function () { getNodeCryptoStub && getNodeCryptoStub.restore(); }; + /** + * Test that the result of `encryptFunction` can be decrypted by `decryptFunction` + * with and without native crypto support. + * @param encryptFunction - `(data: Uint8Array) => encryptFunctionResult` + * @param decryptFunction - `(encryptFunctionResult) => ` + * @param expectNative - whether native usage is expected for the algorithm + */ + const testRountripWithAndWithoutNative = async ( + encryptFunction, + decryptFunction, // (encryptFunctionResult) => decryption result + expectNative + ) => { + const nodeCrypto = util.getNodeCrypto(); + const webCrypto = util.getWebCrypto(); + const data = random.getRandomBytes(16); + + const nativeSpy = webCrypto ? sinonSandbox.spy(webCrypto, 'deriveBits') : sinonSandbox.spy(nodeCrypto, 'createECDH'); // functions used both for encryption and decryption + const nativeResult = await encryptFunction(data); + const expectedNativeEncryptCallCount = nativeSpy.callCount; + disableNative(); + const nonNativeResult = await encryptFunction(data); + expect(nativeSpy.callCount).to.equal(expectedNativeEncryptCallCount); // assert that fallback implementation was called + if (expectNative) { + expect(nativeSpy.calledOnce).to.be.true; + } + + enableNative(); + expect(await decryptFunction(nativeResult)).to.deep.equal(data); + expect(await decryptFunction(nonNativeResult)).to.deep.equal(data); + const expectedNativeCallCount = nativeSpy.callCount; + disableNative(); + expect(await decryptFunction(nativeResult)).to.deep.equal(data); + expect(await decryptFunction(nonNativeResult)).to.deep.equal(data); + expect(nativeSpy.callCount).to.equal(expectedNativeCallCount); // assert that fallback implementation was called + if (expectNative) { + expect(nativeSpy.callCount).to.equal(3); // one encryption + two decryptions + } + }; + + allCurves.forEach(curveName => { it(`${curveName}`, async function () { const nodeCrypto = util.getNodeCrypto(); @@ -344,21 +387,27 @@ export default () => describe('ECDH key exchange @lightweight', function () { const curve = new elliptic_curves.CurveWithOID(curveName); const oid = new OID(curve.oid); const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); - const data = random.getRandomBytes(16); const Q = key_data[curveName].pub; const d = key_data[curveName].priv; - const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q, fingerprint1); - const nativeDecryptSpy = webCrypto ? sinonSandbox.spy(webCrypto, 'deriveBits') : sinonSandbox.spy(nodeCrypto, 'createECDH'); - expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data); - const expectedNativeCallCount = nativeDecryptSpy.callCount; - disableNative(); - expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data); - expect(nativeDecryptSpy.callCount).to.equal(expectedNativeCallCount); // assert that fallback implementation was called - if (expectNativeWeb.has(curveName)) { - expect(nativeDecryptSpy.calledOnce).to.be.true; - } + await testRountripWithAndWithoutNative( + data => ecdh.encrypt(oid, kdfParams, data, Q, fingerprint1), + encryptResult => ecdh.decrypt(oid, kdfParams, encryptResult.publicKey, encryptResult.wrappedKey, Q, d, fingerprint1), + expectNativeWeb.has(curveName) // all major browsers implement x25519 + ); }); }); + + it('Successful exchange x25519 (legacy)', async function () { + const curve = new elliptic_curves.CurveWithOID(openpgp.enums.curve.curve25519Legacy); + const oid = new OID(curve.oid); + const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); + + await testRountripWithAndWithoutNative( + data => ecdh.encrypt(oid, kdfParams, data, Q1, fingerprint1), + encryptResult => ecdh.decrypt(oid, kdfParams, encryptResult.publicKey, encryptResult.wrappedKey, Q1, d1, fingerprint1), + false // all major browsers implement x25519, but webkit linux falls back due to bugs + ); + }); }); }); diff --git a/test/crypto/elliptic.js b/test/crypto/ecdsa_eddsa.js similarity index 59% rename from test/crypto/elliptic.js rename to test/crypto/ecdsa_eddsa.js index 7f98cd3e..5b10621f 100644 --- a/test/crypto/elliptic.js +++ b/test/crypto/ecdsa_eddsa.js @@ -4,17 +4,59 @@ import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/new chaiUse(chaiAsPromised); import openpgp from '../initOpenpgp.js'; -import * as elliptic_curves from '../../src/crypto/public_key/elliptic'; -import { computeDigest } from '../../src/crypto/hash'; -import config from '../../src/config'; +import * as elliptic_curves from '../../src/crypto/public_key/elliptic/index.js'; +import { computeDigest } from '../../src/crypto/hash/index.js'; +import config from '../../src/config/index.js'; import util from '../../src/util.js'; -import elliptic_data from './elliptic_data'; +import elliptic_data from './elliptic_data.js'; import OID from '../../src/type/oid.js'; +import { getRandomBytes } from '../../src/crypto/random.js'; + +/** + * Test that the result of `signFunction` can be verified by `verifyFunction` + * with and without native crypto support. + * @param signFunction - `(data: Uint8Array) => signFunctionResult` + * @param verifyFunction - `(encryptFunctionResult) => ` + * @param expectNative - whether native usage is expected for the algorithm + */ +const testRountripWithAndWithoutNative = async ( + { sinonSandbox, enableNative, disableNative }, + signFunction, + verifyFunction, // (signFunctionResult) => verification result + expectNative +) => { + const nodeCrypto = util.getNodeCrypto(); + const webCrypto = util.getWebCrypto(); + const data = getRandomBytes(16); + const dataDigest = await computeDigest(openpgp.enums.hash.sha512, data); + + const nativeSpySign = webCrypto ? sinonSandbox.spy(webCrypto, 'sign') : sinonSandbox.spy(nodeCrypto, 'createSign'); + const nativeResult = await signFunction(data, dataDigest); + const expectedNativeSignCallCount = nativeSpySign.callCount; + disableNative(); + const nonNativeResult = await signFunction(data, dataDigest); + expect(nativeSpySign.callCount).to.equal(expectedNativeSignCallCount); // assert that fallback implementation was called + if (expectNative) { + expect(nativeSpySign.calledOnce).to.be.true; + } + + const nativeSpyVerify = webCrypto ? sinonSandbox.spy(webCrypto, 'verify') : sinonSandbox.spy(nodeCrypto, 'createVerify'); + enableNative(); + expect(await verifyFunction(nativeResult, data, dataDigest)).to.be.true; + expect(await verifyFunction(nonNativeResult, data, dataDigest)).to.be.true; + const expectedNativeVerifyCallCount = nativeSpyVerify.callCount; + disableNative(); + expect(await verifyFunction(nativeResult, data, dataDigest)).to.be.true; + expect(await verifyFunction(nonNativeResult, data, dataDigest)).be.true; + expect(nativeSpyVerify.callCount).to.equal(expectedNativeVerifyCallCount); // assert that fallback implementation was called + if (expectNative) { + expect(nativeSpyVerify.callCount).to.equal(2); + } +}; const key_data = elliptic_data.key_data; -/* eslint-disable no-invalid-this */ -export default () => describe('Elliptic Curve Cryptography @lightweight', function () { +export default () => describe('ECC signatures', function () { const signature_data = { priv: new Uint8Array([ 0x14, 0x2B, 0xE2, 0xB7, 0x4D, 0xBD, 0x1B, 0x22, @@ -241,19 +283,103 @@ export default () => describe('Elliptic Curve Cryptography @lightweight', functi ); }); const curves = ['secp256k1' , 'nistP256', 'nistP384', 'nistP521', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1']; - curves.forEach(curveName => it(`${curveName} - Sign and verify message`, async function () { + curves.forEach(curveName => it(`${curveName} - Sign and verify message with generated key`, async function () { + const sinonState = { sinonSandbox, enableNative, disableNative }; + const curve = new elliptic_curves.CurveWithOID(curveName); const oid = new OID(curve.oid); - const { Q: keyPublic, secret: keyPrivate } = await elliptic_curves.generate(curveName); - const message = new Uint8Array([ - 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF - ]); - const messageDigest = await computeDigest(openpgp.enums.hash.sha512, message); - await testNativeAndFallback(async () => { - const signature = await elliptic_curves.ecdsa.sign(oid, openpgp.enums.hash.sha512, message, keyPublic, keyPrivate, messageDigest); - await expect(elliptic_curves.ecdsa.verify(oid, openpgp.enums.hash.sha512, signature, message, keyPublic, messageDigest)).to.eventually.be.true; + const expectNativeWeb = new Set(['nistP256', 'nistP384']); // older versions of safari do not implement nistP521 + + const nativeKey = await elliptic_curves.generate(curveName); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.ecdsa.sign(oid, openpgp.enums.hash.sha512, data, nativeKey.Q, nativeKey.secret, dataDigest), + (signature, data, dataDigest) => elliptic_curves.ecdsa.verify(oid, openpgp.enums.hash.sha512, signature, data, nativeKey.Q, dataDigest), + expectNativeWeb.has(curveName) + ); + + sinonSandbox.restore(); // reset spies + disableNative(); + const nonNativeKey = await elliptic_curves.generate(curveName); + enableNative(); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.ecdsa.sign(oid, openpgp.enums.hash.sha512, data, nonNativeKey.Q, nonNativeKey.secret, dataDigest), + (signature, data, dataDigest) => elliptic_curves.ecdsa.verify(oid, openpgp.enums.hash.sha512, signature, data, nonNativeKey.Q, dataDigest), + expectNativeWeb.has(curveName) + ); + })); + }); + + describe('EdDSA signature', function () { + let sinonSandbox; + let getWebCryptoStub; + let getNodeCryptoStub; + + beforeEach(function () { + sinonSandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sinonSandbox.restore(); + }); + + const disableNative = () => { + enableNative(); + // stubbed functions return undefined + getWebCryptoStub = sinonSandbox.stub(util, 'getWebCrypto').returns({ + generateKey: () => { const e = new Error('getWebCrypto is mocked'); e.name = 'NotSupportedError'; throw e; }, + importKey: () => { const e = new Error('getWebCrypto is mocked'); e.name = 'NotSupportedError'; throw e; } }); + getNodeCryptoStub = sinonSandbox.stub(util, 'getNodeCrypto'); + }; + const enableNative = () => { + getWebCryptoStub && getWebCryptoStub.restore(); + getNodeCryptoStub && getNodeCryptoStub.restore(); + }; + + it('ed25519Legacy - Sign and verify message with generated key', async function () { + const sinonState = { sinonSandbox, enableNative, disableNative }; + const curve = new elliptic_curves.CurveWithOID(openpgp.enums.curve.ed25519Legacy); + const oid = new OID(curve.oid); + + const nativeKey = await elliptic_curves.generate(openpgp.enums.curve.ed25519Legacy); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.eddsaLegacy.sign(oid, openpgp.enums.hash.sha512, data, nativeKey.Q, nativeKey.secret, dataDigest), + (signature, data, dataDigest) => elliptic_curves.eddsaLegacy.verify(oid, openpgp.enums.hash.sha512, signature, data, nativeKey.Q, dataDigest) + ); + + sinonSandbox.restore(); // reset spies + disableNative(); + const nonNativeKey = await elliptic_curves.generate(openpgp.enums.curve.ed25519Legacy); + enableNative(); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.eddsaLegacy.sign(oid, openpgp.enums.hash.sha512, data, nonNativeKey.Q, nonNativeKey.secret, dataDigest), + (signature, data, dataDigest) => elliptic_curves.eddsaLegacy.verify(oid, openpgp.enums.hash.sha512, signature, data, nonNativeKey.Q, dataDigest) + ); + }); + + ['ed25519', 'ed448'].forEach(algoName => it(`${algoName} - Sign and verify message with native generated key`, async function () { + const sinonState = { sinonSandbox, enableNative, disableNative }; + const algo = openpgp.enums.publicKey[algoName]; + const nativeKey = await elliptic_curves.eddsa.generate(algo); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.eddsa.sign(algo, openpgp.enums.hash.sha512, data, nativeKey.A, nativeKey.seed, dataDigest), + (signature, data, dataDigest) => elliptic_curves.eddsa.verify(algo, openpgp.enums.hash.sha512, signature, data, nativeKey.A, dataDigest) + ); + + sinonSandbox.restore(); // reset spies + disableNative(); + const nonNativeKey = await elliptic_curves.eddsa.generate(algo); + enableNative(); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.eddsa.sign(algo, openpgp.enums.hash.sha512, data, nonNativeKey.A, nonNativeKey.seed, dataDigest), + (signature, data, dataDigest) => elliptic_curves.eddsa.verify(algo, openpgp.enums.hash.sha512, signature, data, nonNativeKey.A, dataDigest) + ); })); }); }); diff --git a/test/crypto/index.js b/test/crypto/index.js index a4c73ae5..5e7567ff 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -2,7 +2,7 @@ import testBigInteger from './biginteger'; import testCipher from './cipher'; import testHash from './hash'; import testCrypto from './crypto'; -import testElliptic from './elliptic'; +import testElliptic from './ecdsa_eddsa'; import testBrainpoolRFC7027 from './brainpool_rfc7027'; import testECDH from './ecdh'; import testPKCS5 from './pkcs5';