diff --git a/src/lib/waku_message/index.spec.ts b/src/lib/waku_message/index.spec.ts index 7289de649d..9c47f29c88 100644 --- a/src/lib/waku_message/index.spec.ts +++ b/src/lib/waku_message/index.spec.ts @@ -90,6 +90,49 @@ describe('Waku Message', function () { ) ); }); + + it('Waku message round trip binary encryption [symmetric, no signature]', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (payload, key) => { + const msg = await WakuMessage.fromBytes(payload, { + symKey: key, + }); + + const wireBytes = msg.encode(); + const actual = await WakuMessage.decode(wireBytes, [key]); + + expect(actual?.payload).to.deep.equal(payload); + } + ) + ); + }); + + it('Waku message round trip binary encryption [symmetric, signature]', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (payload, sigPrivKey, symKey) => { + const sigPubKey = getPublicKey(sigPrivKey); + + const msg = await WakuMessage.fromBytes(payload, { + symKey: symKey, + sigPrivKey: sigPrivKey, + }); + + const wireBytes = msg.encode(); + const actual = await WakuMessage.decode(wireBytes, [symKey]); + + expect(actual?.payload).to.deep.equal(payload); + expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey); + } + ) + ); + }); }); describe('Interop: Nim', function () { diff --git a/src/lib/waku_message/index.ts b/src/lib/waku_message/index.ts index cf87a6c845..ea335c171e 100644 --- a/src/lib/waku_message/index.ts +++ b/src/lib/waku_message/index.ts @@ -25,10 +25,18 @@ export interface Options { timestamp?: Date; /** * Public Key to use to encrypt the messages using ECIES (Asymmetric Encryption). + * + * @throws if both `encPublicKey` and `symKey` are passed */ encPublicKey?: Uint8Array | string; /** - * Private key to use to sign the message, `encPublicKey` must be provided as only + * Key to use to encrypt the messages using AES (Symmetric Encryption). + * + * @throws if both `encPublicKey` and `symKey` are passed + */ + symKey?: Uint8Array | string; + /** + * Private key to use to sign the message, either `encPublicKey` or `symKey` must be provided as only * encrypted messages are signed. */ sigPrivKey?: Uint8Array; @@ -61,24 +69,37 @@ export class WakuMessage { * * If `opts.sigPrivKey` is passed and version 1 is used, the payload is signed * before encryption. + * + * @throws if both `opts.encPublicKey` and `opt.symKey` are passed */ static async fromBytes( payload: Uint8Array, opts?: Options ): Promise { - const { timestamp, contentTopic, encPublicKey, sigPrivKey } = Object.assign( - { timestamp: new Date(), contentTopic: DefaultContentTopic }, - opts ? opts : {} - ); + const { timestamp, contentTopic, encPublicKey, symKey, sigPrivKey } = + Object.assign( + { timestamp: new Date(), contentTopic: DefaultContentTopic }, + opts ? opts : {} + ); let _payload = payload; let version = DefaultVersion; let sig; + + if (encPublicKey && symKey) { + throw 'Pass either `encPublicKey` or `symKey`, not both.'; + } + if (encPublicKey) { const enc = version_1.clearEncode(_payload, sigPrivKey); _payload = await version_1.encryptAsymmetric(enc.payload, encPublicKey); sig = enc.sig; version = 1; + } else if (symKey) { + const enc = version_1.clearEncode(_payload, sigPrivKey); + _payload = await version_1.encryptSymmetric(enc.payload, symKey); + sig = enc.sig; + version = 1; } return new WakuMessage( @@ -127,7 +148,6 @@ export class WakuMessage { if (protoBuf.version === 1 && protoBuf.payload) { if (decPrivateKeys === undefined) { dbg('Payload is encrypted but no private keys have been provided.'); - return; } @@ -135,10 +155,15 @@ export class WakuMessage { const allResults = await Promise.all( decPrivateKeys.map(async (privateKey) => { try { - return await version_1.decryptAsymmetric(payload, privateKey); + return await version_1.decryptSymmetric(payload, privateKey); } catch (e) { - dbg('Failed to decrypt asymmetric message', e); - return; + dbg('Failed to decrypt message using symmetric encryption', e); + try { + return await version_1.decryptAsymmetric(payload, privateKey); + } catch (e) { + dbg('Failed to decrypt message using asymmetric encryption', e); + return; + } } }) ); diff --git a/src/tests/browser/waku_message.spec.ts b/src/tests/browser/waku_message.spec.ts new file mode 100644 index 0000000000..8657354b3e --- /dev/null +++ b/src/tests/browser/waku_message.spec.ts @@ -0,0 +1,125 @@ +import { expect } from 'chai'; +import fc from 'fast-check'; + +fc.configureGlobal({ + interruptAfterTimeLimit: 1500, + markInterruptAsFailure: true, + numRuns: 10, // Firefox is too slow for 100 (fc default) runs in 2s (mocha default) +}); + +import { WakuMessage } from '../../lib/waku_message'; +import { getPublicKey } from '../../lib/waku_message/version_1'; + +describe('Waku Message', function () { + it('Waku message round trip binary serialization [clear]', async function () { + await fc.assert( + fc.asyncProperty(fc.string(), async (s) => { + const msg = await WakuMessage.fromUtf8String(s); + const binary = msg.encode(); + const actual = await WakuMessage.decode(binary); + + expect(actual).to.deep.equal(msg); + }) + ); + }); + + it('Payload to utf-8', async function () { + await fc.assert( + fc.asyncProperty(fc.string(), async (s) => { + const msg = await WakuMessage.fromUtf8String(s); + const utf8 = msg.payloadAsUtf8; + + return utf8 === s; + }) + ); + }); + + it('Waku message round trip binary encryption [asymmetric, no signature]', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (payload, privKey) => { + const publicKey = getPublicKey(privKey); + + const msg = await WakuMessage.fromBytes(payload, { + encPublicKey: publicKey, + }); + + const wireBytes = msg.encode(); + const actual = await WakuMessage.decode(wireBytes, [privKey]); + + expect(actual?.payload).to.deep.equal(payload); + } + ) + ); + }); + + it('Waku message round trip binary encryption [asymmetric, signature]', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (payload, sigPrivKey, encPrivKey) => { + const sigPubKey = getPublicKey(sigPrivKey); + const encPubKey = getPublicKey(encPrivKey); + + const msg = await WakuMessage.fromBytes(payload, { + encPublicKey: encPubKey, + sigPrivKey: sigPrivKey, + }); + + const wireBytes = msg.encode(); + const actual = await WakuMessage.decode(wireBytes, [encPrivKey]); + + expect(actual?.payload).to.deep.equal(payload); + expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey); + } + ) + ); + }); + + it('Waku message round trip binary encryption [symmetric, no signature]', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (payload, key) => { + const msg = await WakuMessage.fromBytes(payload, { + symKey: key, + }); + + const wireBytes = msg.encode(); + const actual = await WakuMessage.decode(wireBytes, [key]); + + expect(actual?.payload).to.deep.equal(payload); + } + ) + ); + }); + + it('Waku message round trip binary encryption [symmetric, signature]', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (payload, sigPrivKey, symKey) => { + const sigPubKey = getPublicKey(sigPrivKey); + + const msg = await WakuMessage.fromBytes(payload, { + symKey: symKey, + sigPrivKey: sigPrivKey, + }); + + const wireBytes = msg.encode(); + const actual = await WakuMessage.decode(wireBytes, [symKey]); + + expect(actual?.payload).to.deep.equal(payload); + expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey); + } + ) + ); + }); +});