diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d30c16828..c8121d93c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - New `peers` and `randomPeer` methods on `WakuStore` and `WakuLightPush` to have a better idea of available peers; Note that it does not check whether Waku node is currently connected to said peers. - Enable passing decryption private keys to `WakuStore.queryHistory`. +- Test: Introduce testing in browser environment (Chrome) using Karma. +- Add support for Waku Message version 1: Asymmetric encryption, symmetric encryption, and signature of the data. ### Changed - **Breaking**: Auto select peer if none provided for store and light push protocols. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c9612ad08..f9427b7409 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,9 +19,10 @@ To help ensure your PR passes, just run before committing: To build and test this repository, you need: - - [Node.js & npm](https://nodejs.org/en/) - - [bufbuild](https://github.com/bufbuild/buf) (only if changing protobuf files) - - [protoc](https://grpc.io/docs/protoc-installation/) (only if changing protobuf files) + - [Node.js & npm](https://nodejs.org/en/). + - [bufbuild](https://github.com/bufbuild/buf) (only if changing protobuf files). + - [protoc](https://grpc.io/docs/protoc-installation/) (only if changing protobuf files). + - Chrome (for browser testing). To ensure interoperability with [nim-waku](https://github.com/status-im/nim-waku/), some tests are run against a nim-waku node. This is why `nim-waku` is present as a [git submodule](https://git-scm.com/book/en/v2/Git-Tools-Submodules), which itself contain several submodules. @@ -29,10 +30,17 @@ At this stage, it is not possible to exclude nim-waku tests, hence `git submodul To build nim-waku, you also need [Rust](https://www.rust-lang.org/tools/install). +Note that we run tests in both NodeJS and browser environments (both Chrome and Firefox, using [karma](https://karma-runner.github.io/)). +Files named `*.spec.ts` are only run in NodeJS environment; +Files named `*.browser.spec.ts` are run in both NodeJS and browser environment. + ## Guidelines -- Please follow [Chris Beam's commit message guide](https://chris.beams.io/posts/git-commit/), -- Usually best to test new code, +- Please follow [Chris Beam's commit message guide](https://chris.beams.io/posts/git-commit/) for commit patches, +- Please test new code, we use [mocha](https://mochajs.org/), + [chai](https://www.chaijs.com/), + [fast-check](https://github.com/dubzzz/fast-check) + and [karma](https://karma-runner.github.io/). ### Committing Patches diff --git a/karma.conf.js b/karma.conf.js index 35a9e1ff2b..f0b71c49a1 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -3,11 +3,7 @@ process.env.CHROME_BIN = require('puppeteer').executablePath(); module.exports = function (config) { config.set({ frameworks: ['mocha', 'karma-typescript'], - files: [ - 'src/lib/**/*.ts', - 'src/proto/**/*.ts', - 'src/tests/browser/*.spec.ts', - ], + files: ['src/lib/**/*.ts', 'src/proto/**/*.ts'], preprocessors: { '**/*.ts': ['karma-typescript'], }, @@ -21,7 +17,7 @@ module.exports = function (config) { singleRun: true, karmaTypescriptConfig: { bundlerOptions: { - entrypoints: /src\/tests\/browser\/.*\.spec\.ts$/, + entrypoints: /.*\.browser\.spec\.ts$/, }, tsconfig: './tsconfig.karma.json', coverageOptions: { diff --git a/src/lib/waku_message/index.browser.spec.ts b/src/lib/waku_message/index.browser.spec.ts new file mode 100644 index 0000000000..c97ea9e4bc --- /dev/null +++ b/src/lib/waku_message/index.browser.spec.ts @@ -0,0 +1,119 @@ +import { expect } from 'chai'; +import fc from 'fast-check'; + +import { WakuMessage } from '../../lib/waku_message'; +import { getPublicKey } from '../../lib/waku_message/version_1'; + +describe('Waku Message: Browser & Node', 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); + } + ) + ); + }); +}); diff --git a/src/lib/waku_message/index.spec.ts b/src/lib/waku_message/index.spec.ts index 7289de649d..a0f14c6ff4 100644 --- a/src/lib/waku_message/index.spec.ts +++ b/src/lib/waku_message/index.spec.ts @@ -1,6 +1,5 @@ import { expect } from 'chai'; import debug from 'debug'; -import fc from 'fast-check'; // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: No types available import TCP from 'libp2p-tcp'; @@ -21,157 +20,90 @@ import { DefaultContentTopic, WakuMessage } from './index'; const dbg = debug('waku:test:message'); -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); +describe('Waku Message: Node only', function () { + describe('Interop: Nim', function () { + let waku: Waku; + let nimWaku: NimWaku; - expect(actual).to.deep.equal(msg); - }) - ); - }); + beforeEach(async function () { + this.timeout(30_000); - 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; + waku = await Waku.create({ + staticNoiseKey: NOISE_KEY_1, + libp2p: { + addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] }, + modules: { transport: [TCP] }, + }, + }); - return utf8 === s; - }) - ); - }); + const multiAddrWithId = waku.getLocalMultiaddrWithID(); + nimWaku = new NimWaku(makeLogFileName(this)); + await nimWaku.start({ staticnode: multiAddrWithId, rpcPrivate: true }); - 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); - } - ) - ); - }); -}); - -describe('Interop: Nim', function () { - let waku: Waku; - let nimWaku: NimWaku; - - beforeEach(async function () { - this.timeout(30_000); - - waku = await Waku.create({ - staticNoiseKey: NOISE_KEY_1, - libp2p: { - addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] }, - modules: { transport: [TCP] }, - }, + await new Promise((resolve) => + waku.libp2p.pubsub.once('gossipsub:heartbeat', resolve) + ); }); - const multiAddrWithId = waku.getLocalMultiaddrWithID(); - nimWaku = new NimWaku(makeLogFileName(this)); - await nimWaku.start({ staticnode: multiAddrWithId, rpcPrivate: true }); - - await new Promise((resolve) => - waku.libp2p.pubsub.once('gossipsub:heartbeat', resolve) - ); - }); - - afterEach(async function () { - nimWaku ? nimWaku.stop() : null; - waku ? await waku.stop() : null; - }); - - it('JS decrypts nim message [asymmetric, no signature]', async function () { - this.timeout(10000); - await delay(200); - - const messageText = 'Here is an encrypted message.'; - const message: WakuRelayMessage = { - contentTopic: DefaultContentTopic, - payload: Buffer.from(messageText, 'utf-8').toString('hex'), - }; - - const privateKey = generatePrivateKey(); - - waku.relay.addDecryptionPrivateKey(privateKey); - - const receivedMsgPromise: Promise = new Promise((resolve) => { - waku.relay.addObserver(resolve); + afterEach(async function () { + nimWaku ? nimWaku.stop() : null; + waku ? await waku.stop() : null; }); - const publicKey = getPublicKey(privateKey); - dbg('Post message'); - await nimWaku.postAsymmetricMessage(message, publicKey); - - const receivedMsg = await receivedMsgPromise; - - expect(receivedMsg.contentTopic).to.eq(message.contentTopic); - expect(receivedMsg.version).to.eq(1); - expect(receivedMsg.payloadAsUtf8).to.eq(messageText); - }); - - it('Js encrypts message for nim [asymmetric, no signature]', async function () { - this.timeout(5000); - - const keyPair = await nimWaku.getAsymmetricKeyPair(); - const privateKey = hexToBuf(keyPair.privateKey); - const publicKey = hexToBuf(keyPair.publicKey); - - const messageText = 'This is a message I am going to encrypt'; - const message = await WakuMessage.fromUtf8String(messageText, { - encPublicKey: publicKey, - }); - - await waku.relay.send(message); - - let msgs: WakuRelayMessage[] = []; - - while (msgs.length === 0) { + it('JS decrypts nim message [asymmetric, no signature]', async function () { + this.timeout(10000); await delay(200); - msgs = await nimWaku.getAsymmetricMessages(privateKey); - } - expect(msgs[0].contentTopic).to.equal(message.contentTopic); - expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText); + const messageText = 'Here is an encrypted message.'; + const message: WakuRelayMessage = { + contentTopic: DefaultContentTopic, + payload: Buffer.from(messageText, 'utf-8').toString('hex'), + }; + + const privateKey = generatePrivateKey(); + + waku.relay.addDecryptionPrivateKey(privateKey); + + const receivedMsgPromise: Promise = new Promise( + (resolve) => { + waku.relay.addObserver(resolve); + } + ); + + const publicKey = getPublicKey(privateKey); + dbg('Post message'); + await nimWaku.postAsymmetricMessage(message, publicKey); + + const receivedMsg = await receivedMsgPromise; + + expect(receivedMsg.contentTopic).to.eq(message.contentTopic); + expect(receivedMsg.version).to.eq(1); + expect(receivedMsg.payloadAsUtf8).to.eq(messageText); + }); + + it('Js encrypts message for nim [asymmetric, no signature]', async function () { + this.timeout(5000); + + const keyPair = await nimWaku.getAsymmetricKeyPair(); + const privateKey = hexToBuf(keyPair.privateKey); + const publicKey = hexToBuf(keyPair.publicKey); + + const messageText = 'This is a message I am going to encrypt'; + const message = await WakuMessage.fromUtf8String(messageText, { + encPublicKey: publicKey, + }); + + await waku.relay.send(message); + + let msgs: WakuRelayMessage[] = []; + + while (msgs.length === 0) { + await delay(200); + msgs = await nimWaku.getAsymmetricMessages(privateKey); + } + + expect(msgs[0].contentTopic).to.equal(message.contentTopic); + expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText); + }); }); }); diff --git a/src/lib/waku_message/index.ts b/src/lib/waku_message/index.ts index 7a95e3234c..ea335c171e 100644 --- a/src/lib/waku_message/index.ts +++ b/src/lib/waku_message/index.ts @@ -14,9 +14,31 @@ const DefaultVersion = 0; const dbg = debug('waku:message'); export interface Options { + /** + * Content topic to set on the message, defaults to {@link DefaultContentTopic} + * if not passed. + */ contentTopic?: string; + /** + * Timestamp to set on the message, defaults to now if not passed. + */ 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; + /** + * 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; } @@ -47,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( @@ -113,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; } @@ -121,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/lib/waku_message/symmetric/browser.ts b/src/lib/waku_message/symmetric/browser.ts new file mode 100644 index 0000000000..4f092f74da --- /dev/null +++ b/src/lib/waku_message/symmetric/browser.ts @@ -0,0 +1,55 @@ +import { IvSize, SymmetricKeySize } from './index'; + +declare global { + interface Window { + msCrypto?: Crypto; + } + interface Crypto { + webkitSubtle?: SubtleCrypto; + } +} + +const crypto = window.crypto || window.msCrypto; +const subtle: SubtleCrypto = crypto.subtle || crypto.webkitSubtle; + +const Algorithm = { name: 'AES-GCM', length: 128 }; + +if (subtle === undefined) { + throw new Error('Failed to load Subtle CryptoAPI'); +} + +export async function encrypt( + iv: Buffer | Uint8Array, + key: Buffer, + clearText: Buffer +): Promise { + return subtle + .importKey('raw', key, Algorithm, false, ['encrypt']) + .then((cryptoKey) => + subtle.encrypt({ iv, ...Algorithm }, cryptoKey, clearText) + ) + .then(Buffer.from); +} + +export async function decrypt( + iv: Buffer, + key: Buffer, + cipherText: Buffer +): Promise { + return subtle + .importKey('raw', key, Algorithm, false, ['decrypt']) + .then((cryptoKey) => + subtle.decrypt({ iv, ...Algorithm }, cryptoKey, cipherText) + ) + .then(Buffer.from); +} + +export function generateKeyForSymmetricEnc(): Buffer { + return crypto.getRandomValues(Buffer.alloc(SymmetricKeySize)); +} + +export function generateIv(): Uint8Array { + const iv = new Uint8Array(IvSize); + crypto.getRandomValues(iv); + return iv; +} diff --git a/src/lib/waku_message/symmetric/index.ts b/src/lib/waku_message/symmetric/index.ts new file mode 100644 index 0000000000..76a0b2faa1 --- /dev/null +++ b/src/lib/waku_message/symmetric/index.ts @@ -0,0 +1,42 @@ +export const SymmetricKeySize = 32; +export const IvSize = 12; +export const TagSize = 16; + +export interface Symmetric { + /** + * Proceed with symmetric encryption of `clearText` value. + */ + encrypt: ( + iv: Buffer | Uint8Array, + key: Buffer, + clearText: Buffer + ) => Promise; + /** + * Proceed with symmetric decryption of `cipherText` value. + */ + decrypt: (iv: Buffer, key: Buffer, cipherText: Buffer) => Promise; + /** + * Generate a new private key for Symmetric encryption purposes. + */ + generateKeyForSymmetricEnc: () => Buffer; + /** + * Generate an Initialization Vector (iv) for for Symmetric encryption purposes. + */ + generateIv: () => Uint8Array; +} + +export let symmetric: Symmetric = {} as unknown as Symmetric; + +import('./browser') + .then((mod) => { + symmetric = mod; + }) + .catch((eBrowser) => { + import('./node') + .then((mod) => { + symmetric = mod; + }) + .catch((eNode) => { + throw `Could not load any symmetric crypto modules: ${eBrowser}, ${eNode}`; + }); + }); diff --git a/src/lib/waku_message/symmetric/node.ts b/src/lib/waku_message/symmetric/node.ts new file mode 100644 index 0000000000..2ea07f4b5b --- /dev/null +++ b/src/lib/waku_message/symmetric/node.ts @@ -0,0 +1,40 @@ +import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'; + +import { IvSize, SymmetricKeySize, TagSize } from './index'; + +const Algorithm = 'aes-256-gcm'; + +export async function encrypt( + iv: Buffer | Uint8Array, + key: Buffer, + clearText: Buffer +): Promise { + const cipher = createCipheriv(Algorithm, key, iv); + const a = cipher.update(clearText); + const b = cipher.final(); + const tag = cipher.getAuthTag(); + return Buffer.concat([a, b, tag]); +} + +export async function decrypt( + iv: Buffer, + key: Buffer, + data: Buffer +): Promise { + const tagStart = data.length - TagSize; + const cipherText = data.slice(0, tagStart); + const tag = data.slice(tagStart); + const decipher = createDecipheriv(Algorithm, key, iv); + decipher.setAuthTag(tag); + const a = decipher.update(cipherText); + const b = decipher.final(); + return Buffer.concat([a, b]); +} + +export function generateKeyForSymmetricEnc(): Buffer { + return randomBytes(SymmetricKeySize); +} + +export function generateIv(): Buffer { + return randomBytes(IvSize); +} diff --git a/src/lib/waku_message/version_1.spec.ts b/src/lib/waku_message/version_1.browser.spec.ts similarity index 77% rename from src/lib/waku_message/version_1.spec.ts rename to src/lib/waku_message/version_1.browser.spec.ts index d37326658c..89e6b4d169 100644 --- a/src/lib/waku_message/version_1.spec.ts +++ b/src/lib/waku_message/version_1.browser.spec.ts @@ -5,7 +5,9 @@ import { clearDecode, clearEncode, decryptAsymmetric, + decryptSymmetric, encryptAsymmetric, + encryptSymmetric, getPublicKey, } from './version_1'; @@ -54,4 +56,19 @@ describe('Waku Message Version 1', function () { ) ); }); + + it('Symmetric encrypt & Decrypt', async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array(), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (message, key) => { + const enc = await encryptSymmetric(message, key); + const res = await decryptSymmetric(enc, key); + + expect(res).deep.equal(message); + } + ) + ); + }); }); diff --git a/src/lib/waku_message/version_1.ts b/src/lib/waku_message/version_1.ts index 82ddc249b6..7cdbdf68e8 100644 --- a/src/lib/waku_message/version_1.ts +++ b/src/lib/waku_message/version_1.ts @@ -7,6 +7,8 @@ import * as secp256k1 from 'secp256k1'; import { hexToBuf } from '../utils'; +import { IvSize, symmetric } from './symmetric'; + const FlagsLength = 1; const FlagMask = 3; // 0011 const IsSignedMask = 4; // 0100 @@ -102,7 +104,7 @@ export function clearDecode( } /** - * Proceed with Asymmetric encryption of the data as per [26/WAKU-PAYLOAD](rfc.vac.dev/spec/26/). + * Proceed with Asymmetric encryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/). * The data MUST be flags | payload-length | payload | [signature]. * The returned result can be set to `WakuMessage.payload`. * @@ -115,6 +117,12 @@ export async function encryptAsymmetric( return ecies.encrypt(hexToBuf(publicKey), Buffer.from(data)); } +/** + * Proceed with Asymmetric decryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/). + * The return data is expect to be flags | payload-length | payload | [signature]. + * + * @internal + */ export async function decryptAsymmetric( payload: Uint8Array | Buffer, privKey: Uint8Array | Buffer @@ -122,6 +130,47 @@ export async function decryptAsymmetric( return ecies.decrypt(Buffer.from(privKey), Buffer.from(payload)); } +/** + * Proceed with Symmetric encryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/). + * + * @param data The data to encrypt, expected to be `flags | payload-length | payload | [signature]`. + * @param key The key to use for encryption. + * @returns The decrypted data, `cipherText | tag | iv` and can be set to `WakuMessage.payload`. + * + * @internal + */ +export async function encryptSymmetric( + data: Uint8Array | Buffer, + key: Uint8Array | Buffer | string +): Promise { + const iv = symmetric.generateIv(); + + // Returns `cipher | tag` + const cipher = await symmetric.encrypt(iv, hexToBuf(key), Buffer.from(data)); + return Buffer.concat([cipher, iv]); +} + +/** + * Proceed with Symmetric decryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/). + * + * @param payload The cipher data, it is expected to be `cipherText | tag | iv`. + * @param key The key to use for decryption. + * @returns The decrypted data, expected to be `flags | payload-length | payload | [signature]`. + * + * @internal + */ +export async function decryptSymmetric( + payload: Uint8Array | Buffer, + key: Uint8Array | Buffer | string +): Promise { + const data = Buffer.from(payload); + const ivStart = data.length - IvSize; + const cipher = data.slice(0, ivStart); + const iv = data.slice(ivStart); + + return symmetric.decrypt(iv, hexToBuf(key), cipher); +} + /** * Generate a new private key */ @@ -137,7 +186,7 @@ export function getPublicKey(privateKey: Uint8Array | Buffer): Uint8Array { } /** - * Computes the flags & auxiliary-field as per [26/WAKU-PAYLOAD](rfc.vac.dev/spec/26/). + * Computes the flags & auxiliary-field as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/). */ function addPayloadSizeField(msg: Buffer, payload: Uint8Array): Buffer { const fieldSize = getSizeOfPayloadSizeField(payload); diff --git a/src/tests/browser/version_1.spec.ts b/src/tests/browser/version_1.spec.ts deleted file mode 100644 index fd5348e87e..0000000000 --- a/src/tests/browser/version_1.spec.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { expect } from 'chai'; -import fc from 'fast-check'; - -import { - clearDecode, - clearEncode, - decryptAsymmetric, - encryptAsymmetric, - getPublicKey, -} from '../../lib/waku_message/version_1'; - -describe('Waku Message Version 1', function () { - it('Sign & Recover', function () { - fc.assert( - fc.property( - fc.uint8Array(), - fc.uint8Array({ minLength: 32, maxLength: 32 }), - (message, privKey) => { - const enc = clearEncode(message, privKey); - const res = clearDecode(enc.payload); - - const pubKey = getPublicKey(privKey); - - expect(res?.payload).deep.equal( - message, - 'Payload was not encrypted then decrypted correctly' - ); - expect(res?.sig?.publicKey).deep.equal( - pubKey, - 'signature Public key was not recovered from encrypted then decrypted signature' - ); - expect(enc?.sig?.publicKey).deep.equal( - pubKey, - 'Incorrect signature public key was returned when signing the payload' - ); - } - ) - ); - }); - - it('Asymmetric encrypt & Decrypt', async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ minLength: 32, maxLength: 32 }), - async (message, privKey) => { - const publicKey = getPublicKey(privKey); - - const enc = await encryptAsymmetric(message, publicKey); - const res = await decryptAsymmetric(enc, privKey); - - expect(res).deep.equal(message); - } - ) - ); - }); -});