diff --git a/.size-limit.cjs b/.size-limit.cjs index 199a422e9d..6116b79fc7 100644 --- a/.size-limit.cjs +++ b/.size-limit.cjs @@ -6,22 +6,19 @@ module.exports = [ }, { name: "Waku default setup", - path: [ - "packages/create/bundle/index.js", - "packages/core/bundle/lib/wait_for_remote_peer.js" - ], - import: { - "./packages/create/bundle/index.js": "{ createLightNode }", - "./packages/core/bundle/lib/wait_for_remote_peer.js": - "{ waitForRemotePeer }", - "./packages/core/bundle/lib/waku_message/version_0.js": - "{ MessageV0, DecoderV0, EncoderV0 }", - }, + path: "packages/create/bundle/index.js", + import: + "{ createLightNode, waitForRemotePeer, createEncoder, createDecoder }", }, { - name: "Asymmetric, symmetric encryption and signature", - path: "packages/message-encryption/bundle/index.js", - import: "{ MessageV1, AsymEncoder, AsymDecoder, SymEncoder, SymDecoder }", + name: "ECIES encryption", + path: "packages/message-encryption/bundle/ecies.js", + import: "{ generatePrivateKey, createEncoder, createDecoder, DecodedMessage }", + }, + { + name: "Symmetric encryption", + path: "packages/message-encryption/bundle/symmetric.js", + import: "{ generateSymmetricKey, createEncoder, createDecoder, DecodedMessage }", }, { name: "DNS discovery", diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 3f3fe3e0e8..d664636735 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -10,8 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Add `@multiformats/multiaddr` as peer dependency. +- New `createEncoder` and `createDecoder` functions so that the consumer does not deal with Encoder/Decoder classes. -## @waku/core [0.0.6](https://github.com/waku-org/js-waku/compare/@waku/core@0.0.5...@waku/core@0.0.6) (2022-11-18) +### Changed + +- `waitForRemotePeer` must now be directly imported from `@waku/core`. +- `V0` suffix removed from the version 0 objects. +- `createEncoder`/`createDecoder`/`DecodedMessage` for Waku Message Version 0 (no Waku level encryption) can now be imported directly from `@waku/core`. + +## [@waku/core@0.0.6] - 2022-11-18 ### Added @@ -25,22 +32,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `PeerDiscoveryStaticPeer` has been removed, use `@libp2p/bootstrap` instead. -## @waku/core [0.0.5](https://github.com/waku-org/js-waku/compare/@waku/core@0.0.4...@waku/core@0.0.5) (2022-11-11) +## [@waku/core@0.0.5] - 2022-11-11 ### Changed - Bumped `libp2p` to 0.39.5. -## @waku/core [0.0.4](https://github.com/waku-org/js-waku/compare/@waku/core@0.0.3...@waku/core@0.0.4) (2022-11-09) +## [@waku/core@0.0.4] - 2022-11-09 ### Changed - Bumped `libp2p` to 0.39.2. -## @waku/core [0.0.2](https://github.com/waku-org/js-waku/compare/@waku/core@0.0.1...@waku/core@0.0.2) (2022-11-04) +## [@waku/core@0.0.3] - 2022-11-04 + +### Fixed + +- Missing `.js` extension. + +## [@waku/core@0.0.2] - 2022-11-04 ### Changed +- `js-waku` is deprecated, `@waku/core` and other `@waku/*` packages should be used instead. - extract version-1 from chore - extract utils from core - extract dns discovery and enr from core ([f7f28f0](https://github.com/waku-org/js-waku/commit/f7f28f03b01fa5bc89eaeb083b68981169b45c39)) @@ -610,7 +624,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [ReactJS Chat App example](./examples/web-chat). - [Typedoc Documentation](https://js-waku.wakuconnect.dev/). -[unreleased]: https://github.com/status-im/js-waku/compare/v0.30.0...HEAD +[unreleased]: https://github.com/status-im/js-waku/compare/@waku/core@0.0.5...HEAD +[@waku/core@0.0.5]: https://github.com/waku-org/js-waku/compare/@waku/core@0.0.4...@waku/core@0.0.5 +[@waku/core@0.0.4]: https://github.com/waku-org/js-waku/compare/@waku/core@0.0.3...@waku/core@0.0.4 +[@waku/core@0.0.3]: https://github.com/waku-org/js-waku/compare/@waku/core@0.0.2...@waku/core@0.0.3 +[@waku/core@0.0.2]: https://github.com/waku-org/js-waku/compare/@waku/core@0.0.1...@waku/core@0.0.2 +[@waku/core@0.0.1]: https://github.com/waku-org/js-waku/comparev0.30.0...@waku/core@0.0.1 [0.30.0]: https://github.com/status-im/js-waku/compare/v0.29.0...v0.30.0 [0.29.0]: https://github.com/status-im/js-waku/compare/v0.28.0...v0.29.0 [0.28.1]: https://github.com/status-im/js-waku/compare/v0.28.0...v0.28.1 diff --git a/packages/core/package.json b/packages/core/package.json index 43d058a372..5c83f5f243 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -13,10 +13,6 @@ "types": "./dist/lib/predefined_bootstrap_nodes.d.ts", "import": "./dist/lib/predefined_bootstrap_nodes.js" }, - "./lib/wait_for_remote_peer": { - "types": "./dist/lib/wait_for_remote_peer.d.ts", - "import": "./dist/lib/wait_for_remote_peer.js" - }, "./lib/waku_message/version_0": { "types": "./dist/lib/waku_message/version_0.d.ts", "import": "./dist/lib/waku_message/version_0.js" diff --git a/packages/core/rollup.config.js b/packages/core/rollup.config.js index fd47526e18..1a9240bbe7 100644 --- a/packages/core/rollup.config.js +++ b/packages/core/rollup.config.js @@ -1,12 +1,11 @@ -import { nodeResolve } from "@rollup/plugin-node-resolve"; import commonjs from "@rollup/plugin-commonjs"; import json from "@rollup/plugin-json"; +import { nodeResolve } from "@rollup/plugin-node-resolve"; export default { input: { index: "dist/index.js", "lib/predefined_bootstrap_nodes": "dist/lib/predefined_bootstrap_nodes.js", - "lib/wait_for_remote_peer": "dist/lib/wait_for_remote_peer.js", "lib/waku_message/version_0": "dist/lib/waku_message/version_0.js", "lib/waku_message/topic_only_message": "dist/lib/waku_message/topic_only_message.js", diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index dbafc4d4e2..8fc48aaa5e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -4,6 +4,12 @@ export { DefaultUserAgent } from "./lib/waku.js"; export * as proto_message from "./proto/message.js"; export * as proto_topic_only_message from "./proto/topic_only_message.js"; +export { + createEncoder, + createDecoder, + DecodedMessage, +} from "./lib/waku_message/version_0.js"; + export * as waku from "./lib/waku.js"; export { WakuNode } from "./lib/waku.js"; @@ -27,3 +33,5 @@ export { StoreCodec, createCursor, } from "./lib/waku_store/index.js"; + +export { waitForRemotePeer } from "./lib/wait_for_remote_peer.js"; diff --git a/packages/core/src/lib/waku.ts b/packages/core/src/lib/waku.ts index 279846b6df..ea54a64ed5 100644 --- a/packages/core/src/lib/waku.ts +++ b/packages/core/src/lib/waku.ts @@ -12,7 +12,7 @@ import { LightPushCodec, LightPushComponents, } from "./waku_light_push/index.js"; -import { EncoderV0 } from "./waku_message/version_0.js"; +import { createEncoder } from "./waku_message/version_0.js"; import * as relayConstants from "./waku_relay/constants.js"; import { RelayCodecs, RelayPingContentTopic } from "./waku_relay/constants.js"; import { StoreCodec, StoreComponents } from "./waku_store/index.js"; @@ -214,7 +214,7 @@ export class WakuNode implements Waku { const relay = this.relay; if (relay && relayPeriodSecs !== 0) { - const encoder = new EncoderV0(RelayPingContentTopic); + const encoder = createEncoder(RelayPingContentTopic); this.relayKeepAliveTimers[peerIdStr] = setInterval(() => { log("Sending Waku Relay ping message"); relay diff --git a/packages/core/src/lib/waku_light_push/index.ts b/packages/core/src/lib/waku_light_push/index.ts index 60f202aede..2a4eb7db29 100644 --- a/packages/core/src/lib/waku_light_push/index.ts +++ b/packages/core/src/lib/waku_light_push/index.ts @@ -60,7 +60,7 @@ class WakuLightPush implements LightPush { async push( encoder: Encoder, - message: Partial, + message: Message, opts?: ProtocolOptions ): Promise { const pubSubTopic = opts?.pubSubTopic ? opts.pubSubTopic : this.pubSubTopic; diff --git a/packages/core/src/lib/waku_message/version_0.spec.ts b/packages/core/src/lib/waku_message/version_0.spec.ts index 08a88001ef..25ca5d1afb 100644 --- a/packages/core/src/lib/waku_message/version_0.spec.ts +++ b/packages/core/src/lib/waku_message/version_0.spec.ts @@ -1,7 +1,7 @@ import { expect } from "chai"; import fc from "fast-check"; -import { DecoderV0, EncoderV0, MessageV0 } from "./version_0.js"; +import { createDecoder, createEncoder, DecodedMessage } from "./version_0.js"; const TestContentTopic = "/test/1/waku-message/utf8"; @@ -9,11 +9,13 @@ describe("Waku Message version 0", function () { it("Round trip binary serialization", async function () { await fc.assert( fc.asyncProperty(fc.uint8Array({ minLength: 1 }), async (payload) => { - const encoder = new EncoderV0(TestContentTopic); + const encoder = createEncoder(TestContentTopic); const bytes = await encoder.toWire({ payload }); - const decoder = new DecoderV0(TestContentTopic); + const decoder = createDecoder(TestContentTopic); const protoResult = await decoder.fromWireToProtoObj(bytes); - const result = (await decoder.fromProtoObj(protoResult!)) as MessageV0; + const result = (await decoder.fromProtoObj( + protoResult! + )) as DecodedMessage; expect(result.contentTopic).to.eq(TestContentTopic); expect(result.version).to.eq(0); @@ -27,11 +29,13 @@ describe("Waku Message version 0", function () { it("Ephemeral field set to true", async function () { await fc.assert( fc.asyncProperty(fc.uint8Array({ minLength: 1 }), async (payload) => { - const encoder = new EncoderV0(TestContentTopic, true); + const encoder = createEncoder(TestContentTopic, true); const bytes = await encoder.toWire({ payload }); - const decoder = new DecoderV0(TestContentTopic); + const decoder = createDecoder(TestContentTopic); const protoResult = await decoder.fromWireToProtoObj(bytes); - const result = (await decoder.fromProtoObj(protoResult!)) as MessageV0; + const result = (await decoder.fromProtoObj( + protoResult! + )) as DecodedMessage; expect(result.contentTopic).to.eq(TestContentTopic); expect(result.version).to.eq(0); diff --git a/packages/core/src/lib/waku_message/version_0.ts b/packages/core/src/lib/waku_message/version_0.ts index 056504a067..b58251f7d5 100644 --- a/packages/core/src/lib/waku_message/version_0.ts +++ b/packages/core/src/lib/waku_message/version_0.ts @@ -1,7 +1,7 @@ import type { - DecodedMessage, - Decoder, - Encoder, + DecodedMessage as IDecodedMessage, + Decoder as IDecoder, + Encoder as IEncoder, Message, ProtoMessage, RateLimitProof, @@ -16,7 +16,7 @@ const OneMillion = BigInt(1_000_000); export const Version = 0; export { proto }; -export class MessageV0 implements DecodedMessage { +export class DecodedMessage implements IDecodedMessage { constructor(protected proto: proto.WakuMessage) {} get _rawPayload(): Uint8Array | undefined { @@ -71,14 +71,14 @@ export class MessageV0 implements DecodedMessage { } } -export class EncoderV0 implements Encoder { +export class Encoder implements IEncoder { constructor(public contentTopic: string, public ephemeral: boolean = false) {} - async toWire(message: Partial): Promise { + async toWire(message: Message): Promise { return proto.WakuMessage.encode(await this.toProtoObj(message)); } - async toProtoObj(message: Partial): Promise { + async toProtoObj(message: Message): Promise { const timestamp = message.timestamp ?? new Date(); return { @@ -92,8 +92,27 @@ export class EncoderV0 implements Encoder { } } -export class DecoderV0 implements Decoder { - constructor(public contentTopic: string, public ephemeral: boolean = false) {} +/** + * Creates an encoder that encode messages without Waku level encryption or signature. + * + * An encoder is used to encode messages in the [`14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/) + * format to be sent over the Waku network. The resulting encoder can then be + * pass to { @link @waku/interfaces.LightPush.push } or + * { @link @waku/interfaces.Relay.send } to automatically encode outgoing + * messages. + * + * @param contentTopic The content topic to set on outgoing messages. + * @param ephemeral An optional flag to mark message as ephemeral, ie, not to be stored by Waku Store nodes. + */ +export function createEncoder( + contentTopic: string, + ephemeral = false +): Encoder { + return new Encoder(contentTopic, ephemeral); +} + +export class Decoder implements IDecoder { + constructor(public contentTopic: string) {} fromWireToProtoObj(bytes: Uint8Array): Promise { const protoMessage = proto.WakuMessage.decode(bytes); @@ -108,7 +127,7 @@ export class DecoderV0 implements Decoder { }); } - async fromProtoObj(proto: ProtoMessage): Promise { + async fromProtoObj(proto: ProtoMessage): Promise { // https://github.com/status-im/js-waku/issues/921 if (proto.version === undefined) { proto.version = 0; @@ -124,6 +143,21 @@ export class DecoderV0 implements Decoder { return Promise.resolve(undefined); } - return new MessageV0(proto); + return new DecodedMessage(proto); } } + +/** + * Creates an decoder that decode messages without Waku level encryption. + * + * A decoder is used to decode messages from the [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/) + * format when received from the Waku network. The resulting decoder can then be + * pass to { @link @waku/interfaces.Filter.subscribe } or + * { @link @waku/interfaces.Relay.subscribe } to automatically decode incoming + * messages. + * + * @param contentTopic The resulting decoder will only decode messages with this content topic. + */ +export function createDecoder(contentTopic: string): Decoder { + return new Decoder(contentTopic); +} diff --git a/packages/core/src/lib/waku_relay/index.ts b/packages/core/src/lib/waku_relay/index.ts index edd2ecd501..3e8459e6c4 100644 --- a/packages/core/src/lib/waku_relay/index.ts +++ b/packages/core/src/lib/waku_relay/index.ts @@ -99,10 +99,7 @@ class WakuRelay extends GossipSub implements Relay { /** * Send Waku message. */ - public async send( - encoder: Encoder, - message: Partial - ): Promise { + public async send(encoder: Encoder, message: Message): Promise { const msg = await encoder.toWire(message); if (!msg) { log("Failed to encode message, aborting publish"); diff --git a/packages/message-encryption/CHANGELOG.md b/packages/message-encryption/CHANGELOG.md new file mode 100644 index 0000000000..6b08476da8 --- /dev/null +++ b/packages/message-encryption/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Add `@multiformats/multiaddr` as peer dependency. +- New `createEncoder` and `createDecoder` functions so that the consumer does not deal with Encoder/Decoder classes. +- + +### Changed + +- `Asymmetric` renamed to `ECIES` to follow RFC terminology. +- Split `ECIES` and `symmetric` packages, all items are now export from two different paths: `@waku/message-encryption/ecies` and `@waku/message-encryption/symmetric`. +- remove `asym` and `sym` prefix from exported items as they are now differentiated from their export path: `createEncoder`, `createDecoder`, `DecodedMessage`. +- Remove usage for `Partial` with `Message` as `Message`'s field are all optional. + +## [0.0.4] - 2022-11-18 + +### Added + +- Alpha version of `@waku/message-encryption`. + +[unreleased]: https://github.com/waku-org/js-waku/compare/@waku/message-encryption@0.0.4...HEAD +[0.0.4]: https://github.com/waku-org/js-waku/compare/@waku/message-encryption@0.0.3...@waku/message-encryption@0.0.4 +[0.0.3]: https://github.com/waku-org/js-waku/compare/@waku/message-encryption@0.0.2...%40waku/message-encryption@0.0.3 +[0.0.2]: https://github.com/waku-org/js-waku/compare/@waku/message-encryption@0.0.1...%40waku/message-encryption@0.0.2 +[0.0.1]: https://github.com/status-im/js-waku/compare/a20b7809d61ff9a9732aba82b99bbe99f229b935...%40waku/message-encryption%400.0.2 diff --git a/packages/message-encryption/README.md b/packages/message-encryption/README.md new file mode 100644 index 0000000000..68d3adb900 --- /dev/null +++ b/packages/message-encryption/README.md @@ -0,0 +1,65 @@ +# `@waku/message-encryption` + +Provide Waku Message Version 1 payload encryption as defined in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/). + +## Symmetric Encryption + +Symmetric encryption uses a unique key to encrypt and decrypt messages. + +```typescript +import { + createDecoder, + createEncoder, + generateSymmetricKey, +} from "@waku/message-encryption/symmetric"; + +// Generate a random key +const key = generateSymmetricKey(); + +// To send messages, create an encoder +const encoder = createEncoder(contentTopic, key); + +// For example +waku.lightPush.push(encoder, { payload }); + +// To receive messages, create a decoder +const decoder = createDecoder(contentTopic, key); + +// For example +await waku.store.queryOrderedCallback([decoder], (msg) => { + // ... +}); +``` + +## ECIES Encryption + +ECIES encryption enables encryption for a public key and decryption using a private key. + +```typescript +import { + createDecoder, + createEncoder, + generatePrivateKey, + getPublicKey, +} from "@waku/message-encryption/ecies"; + +// Generate a random private key +const privateKey = generatePrivateKey(); + +// Keep the private key secure, provide the public key to the sender +const publicKey = getPublicKey(privateKey); + +// To send messages, create an encoder +const encoder = createEncoder(contentTopic, publicKey); + +// For example +waku.lightPush.push(encoder, { payload }); + +// To receive messages, create a decoder +const decoder = createDecoder(contentTopic, privateKey); + +// For example +await waku.store.queryOrderedCallback([decoder], (msg) => { + // ... +}); +``` diff --git a/packages/message-encryption/package.json b/packages/message-encryption/package.json index 22ff21bd38..c7ebfbad4f 100644 --- a/packages/message-encryption/package.json +++ b/packages/message-encryption/package.json @@ -8,6 +8,14 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./ecies": { + "types": "./dist/ecies.d.ts", + "import": "./dist/ecies.js" + }, + "./symmetric": { + "types": "./dist/symmetric.d.ts", + "import": "./dist/symmetric.js" } }, "type": "module", diff --git a/packages/message-encryption/rollup.config.js b/packages/message-encryption/rollup.config.js index d22d3d231e..f11e6b9c4c 100644 --- a/packages/message-encryption/rollup.config.js +++ b/packages/message-encryption/rollup.config.js @@ -5,6 +5,8 @@ import { nodeResolve } from "@rollup/plugin-node-resolve"; export default { input: { index: "dist/index.js", + ecies: "dist/ecies.js", + symmetric: "dist/symmetric.js", }, output: { dir: "bundle", diff --git a/packages/message-encryption/src/crypto/ecies.ts b/packages/message-encryption/src/crypto/ecies.ts new file mode 100644 index 0000000000..ae3bc460d0 --- /dev/null +++ b/packages/message-encryption/src/crypto/ecies.ts @@ -0,0 +1,194 @@ +import * as secp from "@noble/secp256k1"; +import { concat, hexToBytes } from "@waku/byte-utils"; + +import { getSubtle, randomBytes, sha256 } from "./index.js"; +/** + * HKDF as implemented in go-ethereum. + */ +function kdf(secret: Uint8Array, outputLength: number): Promise { + let ctr = 1; + let written = 0; + let willBeResult = Promise.resolve(new Uint8Array()); + while (written < outputLength) { + const counters = new Uint8Array([ctr >> 24, ctr >> 16, ctr >> 8, ctr]); + const countersSecret = concat( + [counters, secret], + counters.length + secret.length + ); + const willBeHashResult = sha256(countersSecret); + willBeResult = willBeResult.then((result) => + willBeHashResult.then((hashResult) => { + const _hashResult = new Uint8Array(hashResult); + return concat( + [result, _hashResult], + result.length + _hashResult.length + ); + }) + ); + written += 32; + ctr += 1; + } + return willBeResult; +} + +function aesCtrEncrypt( + counter: Uint8Array, + key: ArrayBufferLike, + data: ArrayBufferLike +): Promise { + return getSubtle() + .importKey("raw", key, "AES-CTR", false, ["encrypt"]) + .then((cryptoKey) => + getSubtle().encrypt( + { name: "AES-CTR", counter: counter, length: 128 }, + cryptoKey, + data + ) + ) + .then((bytes) => new Uint8Array(bytes)); +} + +function aesCtrDecrypt( + counter: Uint8Array, + key: ArrayBufferLike, + data: ArrayBufferLike +): Promise { + return getSubtle() + .importKey("raw", key, "AES-CTR", false, ["decrypt"]) + .then((cryptoKey) => + getSubtle().decrypt( + { name: "AES-CTR", counter: counter, length: 128 }, + cryptoKey, + data + ) + ) + .then((bytes) => new Uint8Array(bytes)); +} + +function hmacSha256Sign( + key: ArrayBufferLike, + msg: ArrayBufferLike +): PromiseLike { + const algorithm = { name: "HMAC", hash: { name: "SHA-256" } }; + return getSubtle() + .importKey("raw", key, algorithm, false, ["sign"]) + .then((cryptoKey) => getSubtle().sign(algorithm, cryptoKey, msg)) + .then((bytes) => new Uint8Array(bytes)); +} + +function hmacSha256Verify( + key: ArrayBufferLike, + msg: ArrayBufferLike, + sig: ArrayBufferLike +): Promise { + const algorithm = { name: "HMAC", hash: { name: "SHA-256" } }; + const _key = getSubtle().importKey("raw", key, algorithm, false, ["verify"]); + return _key.then((cryptoKey) => + getSubtle().verify(algorithm, cryptoKey, sig, msg) + ); +} + +/** + * Derive shared secret for given private and public keys. + * + * @param privateKeyA Sender's private key (32 bytes) + * @param publicKeyB Recipient's public key (65 bytes) + * @returns A promise that resolves with the derived shared secret (Px, 32 bytes) + * @throws Error If arguments are invalid + */ +function derive(privateKeyA: Uint8Array, publicKeyB: Uint8Array): Uint8Array { + if (privateKeyA.length !== 32) { + throw new Error( + `Bad private key, it should be 32 bytes but it's actually ${privateKeyA.length} bytes long` + ); + } else if (publicKeyB.length !== 65) { + throw new Error( + `Bad public key, it should be 65 bytes but it's actually ${publicKeyB.length} bytes long` + ); + } else if (publicKeyB[0] !== 4) { + throw new Error("Bad public key, a valid public key would begin with 4"); + } else { + const px = secp.getSharedSecret(privateKeyA, publicKeyB, true); + // Remove the compression prefix + return new Uint8Array(hexToBytes(px).slice(1)); + } +} + +/** + * Encrypt message for given recipient's public key. + * + * @param publicKeyTo Recipient's public key (65 bytes) + * @param msg The message being encrypted + * @return A promise that resolves with the ECIES structure serialized + */ +export async function encrypt( + publicKeyTo: Uint8Array, + msg: Uint8Array +): Promise { + const ephemPrivateKey = randomBytes(32); + + const sharedPx = await derive(ephemPrivateKey, publicKeyTo); + + const hash = await kdf(sharedPx, 32); + + const iv = randomBytes(16); + const encryptionKey = hash.slice(0, 16); + const cipherText = await aesCtrEncrypt(iv, encryptionKey, msg); + + const ivCipherText = concat([iv, cipherText], iv.length + cipherText.length); + + const macKey = await sha256(hash.slice(16)); + const hmac = await hmacSha256Sign(macKey, ivCipherText); + const ephemPublicKey = secp.getPublicKey(ephemPrivateKey, false); + + return concat( + [ephemPublicKey, ivCipherText, hmac], + ephemPublicKey.length + ivCipherText.length + hmac.length + ); +} + +const metaLength = 1 + 64 + 16 + 32; + +/** + * Decrypt message using given private key. + * + * @param privateKey A 32-byte private key of recipient of the message + * @param encrypted ECIES serialized structure (result of ECIES encryption) + * @returns The clear text + * @throws Error If decryption fails + */ +export async function decrypt( + privateKey: Uint8Array, + encrypted: Uint8Array +): Promise { + if (encrypted.length <= metaLength) { + throw new Error( + `Invalid Ciphertext. Data is too small. It should ba at least ${metaLength} bytes` + ); + } else if (encrypted[0] !== 4) { + throw new Error( + `Not a valid ciphertext. It should begin with 4 but actually begin with ${encrypted[0]}` + ); + } else { + // deserialize + const ephemPublicKey = encrypted.slice(0, 65); + const cipherTextLength = encrypted.length - metaLength; + const iv = encrypted.slice(65, 65 + 16); + const cipherAndIv = encrypted.slice(65, 65 + 16 + cipherTextLength); + const ciphertext = cipherAndIv.slice(16); + const msgMac = encrypted.slice(65 + 16 + cipherTextLength); + + // check HMAC + const px = derive(privateKey, ephemPublicKey); + const hash = await kdf(px, 32); + const [encryptionKey, macKey] = await sha256(hash.slice(16)).then( + (macKey) => [hash.slice(0, 16), macKey] + ); + + if (!(await hmacSha256Verify(macKey, cipherAndIv, msgMac))) { + throw new Error("Incorrect MAC"); + } + + return aesCtrDecrypt(iv, encryptionKey, ciphertext); + } +} diff --git a/packages/message-encryption/src/crypto.ts b/packages/message-encryption/src/crypto/index.ts similarity index 97% rename from packages/message-encryption/src/crypto.ts rename to packages/message-encryption/src/crypto/index.ts index 81344b02c1..91eaaca598 100644 --- a/packages/message-encryption/src/crypto.ts +++ b/packages/message-encryption/src/crypto/index.ts @@ -4,7 +4,7 @@ import * as secp from "@noble/secp256k1"; import { concat } from "@waku/byte-utils"; import sha3 from "js-sha3"; -import { Asymmetric, Symmetric } from "./constants.js"; +import { Asymmetric, Symmetric } from "../constants.js"; declare const self: Record | undefined; const crypto: { node?: any; web?: any } = { diff --git a/packages/message-encryption/src/crypto/symmetric.ts b/packages/message-encryption/src/crypto/symmetric.ts new file mode 100644 index 0000000000..35f69f4558 --- /dev/null +++ b/packages/message-encryption/src/crypto/symmetric.ts @@ -0,0 +1,33 @@ +import { Symmetric } from "../constants.js"; + +import { getSubtle, randomBytes } from "./index.js"; + +export async function encrypt( + iv: Uint8Array, + key: Uint8Array, + clearText: Uint8Array +): Promise { + return getSubtle() + .importKey("raw", key, Symmetric.algorithm, false, ["encrypt"]) + .then((cryptoKey) => + getSubtle().encrypt({ iv, ...Symmetric.algorithm }, cryptoKey, clearText) + ) + .then((cipher) => new Uint8Array(cipher)); +} + +export async function decrypt( + iv: Uint8Array, + key: Uint8Array, + cipherText: Uint8Array +): Promise { + return getSubtle() + .importKey("raw", key, Symmetric.algorithm, false, ["decrypt"]) + .then((cryptoKey) => + getSubtle().decrypt({ iv, ...Symmetric.algorithm }, cryptoKey, cipherText) + ) + .then((clear) => new Uint8Array(clear)); +} + +export function generateIv(): Uint8Array { + return randomBytes(Symmetric.ivSize); +} diff --git a/packages/message-encryption/src/ecies.spec.ts b/packages/message-encryption/src/ecies.spec.ts new file mode 100644 index 0000000000..0b1a30d25d --- /dev/null +++ b/packages/message-encryption/src/ecies.spec.ts @@ -0,0 +1,71 @@ +import { expect } from "chai"; +import fc from "fast-check"; + +import { getPublicKey } from "./crypto/index.js"; +import { createDecoder, createEncoder } from "./ecies.js"; + +const TestContentTopic = "/test/1/waku-message/utf8"; + +describe("Ecies Encryption", function () { + it("Round trip binary encryption [ecies, no signature]", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (payload, privateKey) => { + const publicKey = getPublicKey(privateKey); + + const encoder = createEncoder(TestContentTopic, publicKey); + const bytes = await encoder.toWire({ payload }); + + const decoder = createDecoder(TestContentTopic, privateKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(protoResult); + if (!result) throw "Failed to decode"; + + expect(result.contentTopic).to.equal(TestContentTopic); + expect(result.version).to.equal(1); + expect(result?.payload).to.deep.equal(payload); + expect(result.signature).to.be.undefined; + expect(result.signaturePublicKey).to.be.undefined; + } + ) + ); + }); + + it("R trip binary encryption [ecies, signature]", async function () { + this.timeout(4000); + + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (payload, alicePrivateKey, bobPrivateKey) => { + const alicePublicKey = getPublicKey(alicePrivateKey); + const bobPublicKey = getPublicKey(bobPrivateKey); + + const encoder = createEncoder( + TestContentTopic, + bobPublicKey, + alicePrivateKey + ); + const bytes = await encoder.toWire({ payload }); + + const decoder = createDecoder(TestContentTopic, bobPrivateKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(protoResult); + if (!result) throw "Failed to decode"; + + expect(result.contentTopic).to.equal(TestContentTopic); + expect(result.version).to.equal(1); + expect(result?.payload).to.deep.equal(payload); + expect(result.signature).to.not.be.undefined; + expect(result.signaturePublicKey).to.deep.eq(alicePublicKey); + } + ) + ); + }); +}); diff --git a/packages/message-encryption/src/ecies.ts b/packages/message-encryption/src/ecies.ts index b83d2a1d72..6b73c5f38c 100644 --- a/packages/message-encryption/src/ecies.ts +++ b/packages/message-encryption/src/ecies.ts @@ -1,194 +1,171 @@ -import * as secp from "@noble/secp256k1"; -import { concat, hexToBytes } from "@waku/byte-utils"; +import { + Decoder as DecoderV0, + proto, +} from "@waku/core/lib/waku_message/version_0"; +import type { + Decoder as IDecoder, + Encoder as IEncoder, + Message, + ProtoMessage, +} from "@waku/interfaces"; +import debug from "debug"; -import { getSubtle, randomBytes, sha256 } from "./crypto.js"; -/** - * HKDF as implemented in go-ethereum. - */ -function kdf(secret: Uint8Array, outputLength: number): Promise { - let ctr = 1; - let written = 0; - let willBeResult = Promise.resolve(new Uint8Array()); - while (written < outputLength) { - const counters = new Uint8Array([ctr >> 24, ctr >> 16, ctr >> 8, ctr]); - const countersSecret = concat( - [counters, secret], - counters.length + secret.length - ); - const willBeHashResult = sha256(countersSecret); - willBeResult = willBeResult.then((result) => - willBeHashResult.then((hashResult) => { - const _hashResult = new Uint8Array(hashResult); - return concat( - [result, _hashResult], - result.length + _hashResult.length - ); - }) - ); - written += 32; - ctr += 1; +import { + decryptAsymmetric, + encryptAsymmetric, + postCipher, + preCipher, +} from "./waku_payload.js"; + +import { + DecodedMessage, + generatePrivateKey, + getPublicKey, + OneMillion, + Version, +} from "./index.js"; + +export { DecodedMessage, generatePrivateKey, getPublicKey }; + +const log = debug("waku:message-encryption:ecies"); + +class Encoder implements IEncoder { + constructor( + public contentTopic: string, + private publicKey: Uint8Array, + private sigPrivKey?: Uint8Array, + public ephemeral: boolean = false + ) {} + + async toWire(message: Message): Promise { + const protoMessage = await this.toProtoObj(message); + if (!protoMessage) return; + + return proto.WakuMessage.encode(protoMessage); } - return willBeResult; -} -function aesCtrEncrypt( - counter: Uint8Array, - key: ArrayBufferLike, - data: ArrayBufferLike -): Promise { - return getSubtle() - .importKey("raw", key, "AES-CTR", false, ["encrypt"]) - .then((cryptoKey) => - getSubtle().encrypt( - { name: "AES-CTR", counter: counter, length: 128 }, - cryptoKey, - data - ) - ) - .then((bytes) => new Uint8Array(bytes)); -} + async toProtoObj(message: Message): Promise { + const timestamp = message.timestamp ?? new Date(); + if (!message.payload) { + log("No payload to encrypt, skipping: ", message); + return; + } + const preparedPayload = await preCipher(message.payload, this.sigPrivKey); -function aesCtrDecrypt( - counter: Uint8Array, - key: ArrayBufferLike, - data: ArrayBufferLike -): Promise { - return getSubtle() - .importKey("raw", key, "AES-CTR", false, ["decrypt"]) - .then((cryptoKey) => - getSubtle().decrypt( - { name: "AES-CTR", counter: counter, length: 128 }, - cryptoKey, - data - ) - ) - .then((bytes) => new Uint8Array(bytes)); -} + const payload = await encryptAsymmetric(preparedPayload, this.publicKey); -function hmacSha256Sign( - key: ArrayBufferLike, - msg: ArrayBufferLike -): PromiseLike { - const algorithm = { name: "HMAC", hash: { name: "SHA-256" } }; - return getSubtle() - .importKey("raw", key, algorithm, false, ["sign"]) - .then((cryptoKey) => getSubtle().sign(algorithm, cryptoKey, msg)) - .then((bytes) => new Uint8Array(bytes)); -} - -function hmacSha256Verify( - key: ArrayBufferLike, - msg: ArrayBufferLike, - sig: ArrayBufferLike -): Promise { - const algorithm = { name: "HMAC", hash: { name: "SHA-256" } }; - const _key = getSubtle().importKey("raw", key, algorithm, false, ["verify"]); - return _key.then((cryptoKey) => - getSubtle().verify(algorithm, cryptoKey, sig, msg) - ); -} - -/** - * Derive shared secret for given private and public keys. - * - * @param privateKeyA Sender's private key (32 bytes) - * @param publicKeyB Recipient's public key (65 bytes) - * @returns A promise that resolves with the derived shared secret (Px, 32 bytes) - * @throws Error If arguments are invalid - */ -function derive(privateKeyA: Uint8Array, publicKeyB: Uint8Array): Uint8Array { - if (privateKeyA.length !== 32) { - throw new Error( - `Bad private key, it should be 32 bytes but it's actually ${privateKeyA.length} bytes long` - ); - } else if (publicKeyB.length !== 65) { - throw new Error( - `Bad public key, it should be 65 bytes but it's actually ${publicKeyB.length} bytes long` - ); - } else if (publicKeyB[0] !== 4) { - throw new Error("Bad public key, a valid public key would begin with 4"); - } else { - const px = secp.getSharedSecret(privateKeyA, publicKeyB, true); - // Remove the compression prefix - return new Uint8Array(hexToBytes(px).slice(1)); + return { + payload, + version: Version, + contentTopic: this.contentTopic, + timestamp: BigInt(timestamp.valueOf()) * OneMillion, + rateLimitProof: message.rateLimitProof, + ephemeral: this.ephemeral, + }; } } /** - * Encrypt message for given recipient's public key. + * Creates an encoder that encrypts messages using ECIES for the given public, + * as defined in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/). * - * @param publicKeyTo Recipient's public key (65 bytes) - * @param msg The message being encrypted - * @return A promise that resolves with the ECIES structure serialized + * An encoder is used to encode messages in the [`14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/) + * format to be sent over the Waku network. The resulting encoder can then be + * pass to { @link @waku/interfaces.LightPush.push } or + * { @link @waku/interfaces.Relay.send } to automatically encrypt + * and encode outgoing messages. + * + * The payload can optionally be signed with the given private key as defined + * in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/). + * + * @param contentTopic The content topic to set on outgoing messages. + * @param publicKey The public key to encrypt the payload for. + * @param sigPrivKey An optional private key to used to sign the payload before encryption. + * @param ephemeral An optional flag to mark message as ephemeral, ie, not to be stored by Waku Store nodes. */ -export async function encrypt( - publicKeyTo: Uint8Array, - msg: Uint8Array -): Promise { - const ephemPrivateKey = randomBytes(32); - - const sharedPx = await derive(ephemPrivateKey, publicKeyTo); - - const hash = await kdf(sharedPx, 32); - - const iv = randomBytes(16); - const encryptionKey = hash.slice(0, 16); - const cipherText = await aesCtrEncrypt(iv, encryptionKey, msg); - - const ivCipherText = concat([iv, cipherText], iv.length + cipherText.length); - - const macKey = await sha256(hash.slice(16)); - const hmac = await hmacSha256Sign(macKey, ivCipherText); - const ephemPublicKey = secp.getPublicKey(ephemPrivateKey, false); - - return concat( - [ephemPublicKey, ivCipherText, hmac], - ephemPublicKey.length + ivCipherText.length + hmac.length - ); +export function createEncoder( + contentTopic: string, + publicKey: Uint8Array, + sigPrivKey?: Uint8Array, + ephemeral = false +): Encoder { + return new Encoder(contentTopic, publicKey, sigPrivKey, ephemeral); } -const metaLength = 1 + 64 + 16 + 32; +class Decoder extends DecoderV0 implements IDecoder { + constructor(contentTopic: string, private privateKey: Uint8Array) { + super(contentTopic); + } -/** - * Decrypt message using given private key. - * - * @param privateKey A 32-byte private key of recipient of the message - * @param encrypted ECIES serialized structure (result of ECIES encryption) - * @returns The clear text - * @throws Error If decryption fails - */ -export async function decrypt( - privateKey: Uint8Array, - encrypted: Uint8Array -): Promise { - if (encrypted.length <= metaLength) { - throw new Error( - `Invalid Ciphertext. Data is too small. It should ba at least ${metaLength} bytes` - ); - } else if (encrypted[0] !== 4) { - throw new Error( - `Not a valid ciphertext. It should begin with 4 but actually begin with ${encrypted[0]}` - ); - } else { - // deserialize - const ephemPublicKey = encrypted.slice(0, 65); - const cipherTextLength = encrypted.length - metaLength; - const iv = encrypted.slice(65, 65 + 16); - const cipherAndIv = encrypted.slice(65, 65 + 16 + cipherTextLength); - const ciphertext = cipherAndIv.slice(16); - const msgMac = encrypted.slice(65 + 16 + cipherTextLength); + async fromProtoObj( + protoMessage: ProtoMessage + ): Promise { + const cipherPayload = protoMessage.payload; - // check HMAC - const px = derive(privateKey, ephemPublicKey); - const hash = await kdf(px, 32); - const [encryptionKey, macKey] = await sha256(hash.slice(16)).then( - (macKey) => [hash.slice(0, 16), macKey] - ); - - if (!(await hmacSha256Verify(macKey, cipherAndIv, msgMac))) { - throw new Error("Incorrect MAC"); + if (protoMessage.version !== Version) { + log( + "Failed to decrypt due to incorrect version, expected:", + Version, + ", actual:", + protoMessage.version + ); + return; } - return aesCtrDecrypt(iv, encryptionKey, ciphertext); + let payload; + if (!cipherPayload) { + log(`No payload to decrypt for contentTopic ${this.contentTopic}`); + return; + } + + try { + payload = await decryptAsymmetric(cipherPayload, this.privateKey); + } catch (e) { + log( + `Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`, + e + ); + return; + } + + if (!payload) { + log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`); + return; + } + + const res = await postCipher(payload); + + if (!res) { + log(`Failed to decode payload for contentTopic ${this.contentTopic}`); + return; + } + + log("Message decrypted", protoMessage); + return new DecodedMessage( + protoMessage, + res.payload, + res.sig?.signature, + res.sig?.publicKey + ); } } + +/** + * Creates a decoder that decrypts messages using ECIES, using the given private + * key as defined in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/). + * + * A decoder is used to decode messages from the [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/) + * format when received from the Waku network. The resulting decoder can then be + * pass to { @link @waku/interfaces.Filter.subscribe } or + * { @link @waku/interfaces.Relay.subscribe } to automatically decrypt and + * decode incoming messages. + * + * @param contentTopic The resulting decoder will only decode messages with this content topic. + * @param privateKey The private key used to decrypt the message. + */ +export function createDecoder( + contentTopic: string, + privateKey: Uint8Array +): Decoder { + return new Decoder(contentTopic, privateKey); +} diff --git a/packages/message-encryption/src/index.spec.ts b/packages/message-encryption/src/index.spec.ts deleted file mode 100644 index 2ecea93ddf..0000000000 --- a/packages/message-encryption/src/index.spec.ts +++ /dev/null @@ -1,208 +0,0 @@ -import { expect } from "chai"; -import fc from "fast-check"; - -import { getPublicKey } from "./crypto.js"; - -import { - AsymDecoder, - AsymEncoder, - decryptAsymmetric, - decryptSymmetric, - encryptAsymmetric, - encryptSymmetric, - postCipher, - preCipher, - SymDecoder, - SymEncoder, -} from "./index.js"; - -const TestContentTopic = "/test/1/waku-message/utf8"; - -describe("Waku Message version 1", function () { - it("Round trip binary encryption [asymmetric, no signature]", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - async (payload, privateKey) => { - const publicKey = getPublicKey(privateKey); - - const encoder = new AsymEncoder(TestContentTopic, publicKey); - const bytes = await encoder.toWire({ payload }); - - const decoder = new AsymDecoder(TestContentTopic, privateKey); - const protoResult = await decoder.fromWireToProtoObj(bytes!); - if (!protoResult) throw "Failed to proto decode"; - const result = await decoder.fromProtoObj(protoResult); - if (!result) throw "Failed to decode"; - - expect(result.contentTopic).to.equal(TestContentTopic); - expect(result.version).to.equal(1); - expect(result?.payload).to.deep.equal(payload); - expect(result.signature).to.be.undefined; - expect(result.signaturePublicKey).to.be.undefined; - } - ) - ); - }); - - it("R trip binary encryption [asymmetric, signature]", async function () { - this.timeout(4000); - - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - async (payload, alicePrivateKey, bobPrivateKey) => { - const alicePublicKey = getPublicKey(alicePrivateKey); - const bobPublicKey = getPublicKey(bobPrivateKey); - - const encoder = new AsymEncoder( - TestContentTopic, - bobPublicKey, - alicePrivateKey - ); - const bytes = await encoder.toWire({ payload }); - - const decoder = new AsymDecoder(TestContentTopic, bobPrivateKey); - const protoResult = await decoder.fromWireToProtoObj(bytes!); - if (!protoResult) throw "Failed to proto decode"; - const result = await decoder.fromProtoObj(protoResult); - if (!result) throw "Failed to decode"; - - expect(result.contentTopic).to.equal(TestContentTopic); - expect(result.version).to.equal(1); - expect(result?.payload).to.deep.equal(payload); - expect(result.signature).to.not.be.undefined; - expect(result.signaturePublicKey).to.deep.eq(alicePublicKey); - } - ) - ); - }); - - it("Round trip binary encryption [symmetric, no signature]", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - async (payload, symKey) => { - const encoder = new SymEncoder(TestContentTopic, symKey); - const bytes = await encoder.toWire({ payload }); - - const decoder = new SymDecoder(TestContentTopic, symKey); - const protoResult = await decoder.fromWireToProtoObj(bytes!); - if (!protoResult) throw "Failed to proto decode"; - const result = await decoder.fromProtoObj(protoResult); - if (!result) throw "Failed to decode"; - - expect(result.contentTopic).to.equal(TestContentTopic); - expect(result.version).to.equal(1); - expect(result?.payload).to.deep.equal(payload); - expect(result.signature).to.be.undefined; - expect(result.signaturePublicKey).to.be.undefined; - } - ) - ); - }); - - it("Round trip binary encryption [symmetric, signature]", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), - async (payload, sigPrivKey, symKey) => { - const sigPubKey = getPublicKey(sigPrivKey); - - const encoder = new SymEncoder(TestContentTopic, symKey, sigPrivKey); - const bytes = await encoder.toWire({ payload }); - - const decoder = new SymDecoder(TestContentTopic, symKey); - const protoResult = await decoder.fromWireToProtoObj(bytes!); - if (!protoResult) throw "Failed to proto decode"; - const result = await decoder.fromProtoObj(protoResult); - if (!result) throw "Failed to decode"; - - expect(result.contentTopic).to.equal(TestContentTopic); - expect(result.version).to.equal(1); - expect(result?.payload).to.deep.equal(payload); - expect(result.signature).to.not.be.undefined; - expect(result.signaturePublicKey).to.deep.eq(sigPubKey); - } - ) - ); - }); -}); - -describe("Encryption helpers", () => { - it("Asymmetric encrypt & decrypt", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array({ minLength: 1 }), - fc.uint8Array({ min: 1, 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); - } - ) - ); - }); - - 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); - } - ) - ); - }); - - it("pre and post cipher", async function () { - await fc.assert( - fc.asyncProperty(fc.uint8Array(), async (message) => { - const enc = await preCipher(message); - const res = postCipher(enc); - - expect(res?.payload).deep.equal( - message, - "Payload was not encrypted then decrypted correctly" - ); - }) - ); - }); - - it("Sign & Recover", async function () { - await fc.assert( - fc.asyncProperty( - fc.uint8Array(), - fc.uint8Array({ minLength: 32, maxLength: 32 }), - async (message, sigPrivKey) => { - const sigPubKey = getPublicKey(sigPrivKey); - - const enc = await preCipher(message, sigPrivKey); - const res = postCipher(enc); - - expect(res?.payload).deep.equal( - message, - "Payload was not encrypted then decrypted correctly" - ); - expect(res?.sig?.publicKey).deep.equal( - sigPubKey, - "signature Public key was not recovered from encrypted then decrypted signature" - ); - } - ) - ); - }); -}); diff --git a/packages/message-encryption/src/index.ts b/packages/message-encryption/src/index.ts index fc60c60a99..7705df57c3 100644 --- a/packages/message-encryption/src/index.ts +++ b/packages/message-encryption/src/index.ts @@ -1,42 +1,22 @@ -import * as secp from "@noble/secp256k1"; -import { concat, hexToBytes } from "@waku/byte-utils"; import { - DecoderV0, - MessageV0, + DecodedMessage as DecodedMessageV0, proto, } from "@waku/core/lib/waku_message/version_0"; -import type { - DecodedMessage, - Decoder, - Encoder, - Message, - ProtoMessage, -} from "@waku/interfaces"; -import debug from "debug"; +import type { DecodedMessage as IDecodedMessage } from "@waku/interfaces"; -import { Symmetric } from "./constants.js"; import { generatePrivateKey, generateSymmetricKey, getPublicKey, - keccak256, - randomBytes, - sign, -} from "./crypto.js"; -import * as ecies from "./ecies.js"; -import * as symmetric from "./symmetric.js"; +} from "./crypto/index.js"; -const log = debug("waku:message:version-1"); - -const FlagsLength = 1; -const FlagMask = 3; // 0011 -const IsSignedMask = 4; // 0100 -const PaddingTarget = 256; -const SignatureLength = 65; -const OneMillion = BigInt(1_000_000); +export const OneMillion = BigInt(1_000_000); export { generatePrivateKey, generateSymmetricKey, getPublicKey }; +export * as ecies from "./ecies.js"; +export * as symmetric from "./symmetric.js"; + export const Version = 1; export type Signature = { @@ -44,7 +24,10 @@ export type Signature = { publicKey: Uint8Array | undefined; }; -export class MessageV1 extends MessageV0 implements DecodedMessage { +export class DecodedMessage + extends DecodedMessageV0 + implements IDecodedMessage +{ private readonly _decodedPayload: Uint8Array; constructor( @@ -61,418 +44,3 @@ export class MessageV1 extends MessageV0 implements DecodedMessage { return this._decodedPayload; } } - -export class AsymEncoder implements Encoder { - constructor( - public contentTopic: string, - private publicKey: Uint8Array, - private sigPrivKey?: Uint8Array, - public ephemeral: boolean = false - ) {} - - async toWire(message: Partial): Promise { - const protoMessage = await this.toProtoObj(message); - if (!protoMessage) return; - - return proto.WakuMessage.encode(protoMessage); - } - - async toProtoObj( - message: Partial - ): Promise { - const timestamp = message.timestamp ?? new Date(); - if (!message.payload) { - log("No payload to encrypt, skipping: ", message); - return; - } - const preparedPayload = await preCipher(message.payload, this.sigPrivKey); - - const payload = await encryptAsymmetric(preparedPayload, this.publicKey); - - return { - payload, - version: Version, - contentTopic: this.contentTopic, - timestamp: BigInt(timestamp.valueOf()) * OneMillion, - rateLimitProof: message.rateLimitProof, - ephemeral: this.ephemeral, - }; - } -} - -export class SymEncoder implements Encoder { - constructor( - public contentTopic: string, - private symKey: Uint8Array, - private sigPrivKey?: Uint8Array, - public ephemeral: boolean = false - ) {} - - async toWire(message: Partial): Promise { - const protoMessage = await this.toProtoObj(message); - if (!protoMessage) return; - - return proto.WakuMessage.encode(protoMessage); - } - - async toProtoObj( - message: Partial - ): Promise { - const timestamp = message.timestamp ?? new Date(); - if (!message.payload) { - log("No payload to encrypt, skipping: ", message); - return; - } - const preparedPayload = await preCipher(message.payload, this.sigPrivKey); - - const payload = await encryptSymmetric(preparedPayload, this.symKey); - return { - payload, - version: Version, - contentTopic: this.contentTopic, - timestamp: BigInt(timestamp.valueOf()) * OneMillion, - rateLimitProof: message.rateLimitProof, - ephemeral: this.ephemeral, - }; - } -} - -export class AsymDecoder extends DecoderV0 implements Decoder { - constructor(contentTopic: string, private privateKey: Uint8Array) { - super(contentTopic); - } - - async fromProtoObj( - protoMessage: ProtoMessage - ): Promise { - const cipherPayload = protoMessage.payload; - - if (protoMessage.version !== Version) { - log( - "Failed to decrypt due to incorrect version, expected:", - Version, - ", actual:", - protoMessage.version - ); - return; - } - - let payload; - if (!cipherPayload) { - log(`No payload to decrypt for contentTopic ${this.contentTopic}`); - return; - } - - try { - payload = await decryptAsymmetric(cipherPayload, this.privateKey); - } catch (e) { - log( - `Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`, - e - ); - return; - } - - if (!payload) { - log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`); - return; - } - - const res = await postCipher(payload); - - if (!res) { - log(`Failed to decode payload for contentTopic ${this.contentTopic}`); - return; - } - - log("Message decrypted", protoMessage); - return new MessageV1( - protoMessage, - res.payload, - res.sig?.signature, - res.sig?.publicKey - ); - } -} - -export class SymDecoder extends DecoderV0 implements Decoder { - constructor(contentTopic: string, private symKey: Uint8Array) { - super(contentTopic); - } - - async fromProtoObj( - protoMessage: ProtoMessage - ): Promise { - const cipherPayload = protoMessage.payload; - - if (protoMessage.version !== Version) { - log( - "Failed to decrypt due to incorrect version, expected:", - Version, - ", actual:", - protoMessage.version - ); - return; - } - - let payload; - if (!cipherPayload) { - log(`No payload to decrypt for contentTopic ${this.contentTopic}`); - return; - } - - try { - payload = await decryptSymmetric(cipherPayload, this.symKey); - } catch (e) { - log( - `Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`, - e - ); - return; - } - - if (!payload) { - log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`); - return; - } - - const res = await postCipher(payload); - - if (!res) { - log(`Failed to decode payload for contentTopic ${this.contentTopic}`); - return; - } - - log("Message decrypted", protoMessage); - return new MessageV1( - protoMessage, - res.payload, - res.sig?.signature, - res.sig?.publicKey - ); - } -} - -function getSizeOfPayloadSizeField(message: Uint8Array): number { - const messageDataView = new DataView(message.buffer); - return messageDataView.getUint8(0) & FlagMask; -} - -function getPayloadSize( - message: Uint8Array, - sizeOfPayloadSizeField: number -): number { - let payloadSizeBytes = message.slice(1, 1 + sizeOfPayloadSizeField); - // int 32 == 4 bytes - if (sizeOfPayloadSizeField < 4) { - // If less than 4 bytes pad right (Little Endian). - payloadSizeBytes = concat( - [payloadSizeBytes, new Uint8Array(4 - sizeOfPayloadSizeField)], - 4 - ); - } - const payloadSizeDataView = new DataView(payloadSizeBytes.buffer); - return payloadSizeDataView.getInt32(0, true); -} - -function isMessageSigned(message: Uint8Array): boolean { - const messageDataView = new DataView(message.buffer); - return (messageDataView.getUint8(0) & IsSignedMask) == IsSignedMask; -} - -/** - * 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`. - * - * @internal - */ -export async function encryptAsymmetric( - data: Uint8Array, - publicKey: Uint8Array | string -): Promise { - return ecies.encrypt(hexToBytes(publicKey), data); -} - -/** - * Proceed with Asymmetric decryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/). - * The returned data is expected to be `flags | payload-length | payload | [signature]`. - * - * @internal - */ -export async function decryptAsymmetric( - payload: Uint8Array, - privKey: Uint8Array -): Promise { - return ecies.decrypt(privKey, 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, - key: Uint8Array | string -): Promise { - const iv = symmetric.generateIv(); - - // Returns `cipher | tag` - const cipher = await symmetric.encrypt(iv, hexToBytes(key), data); - return 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, - key: Uint8Array | string -): Promise { - const ivStart = payload.length - Symmetric.ivSize; - const cipher = payload.slice(0, ivStart); - const iv = payload.slice(ivStart); - - return symmetric.decrypt(iv, hexToBytes(key), cipher); -} - -/** - * Computes the flags & auxiliary-field as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/). - */ -function addPayloadSizeField(msg: Uint8Array, payload: Uint8Array): Uint8Array { - const fieldSize = computeSizeOfPayloadSizeField(payload); - let field = new Uint8Array(4); - const fieldDataView = new DataView(field.buffer); - fieldDataView.setUint32(0, payload.length, true); - field = field.slice(0, fieldSize); - msg = concat([msg, field]); - msg[0] |= fieldSize; - return msg; -} - -/** - * Returns the size of the auxiliary-field which in turns contains the payload size - */ -function computeSizeOfPayloadSizeField(payload: Uint8Array): number { - let s = 1; - for (let i = payload.length; i >= 256; i /= 256) { - s++; - } - return s; -} - -function validateDataIntegrity( - value: Uint8Array, - expectedSize: number -): boolean { - if (value.length !== expectedSize) { - return false; - } - - return expectedSize <= 3 || value.findIndex((i) => i !== 0) !== -1; -} - -function getSignature(message: Uint8Array): Uint8Array { - return message.slice(message.length - SignatureLength, message.length); -} - -function getHash(message: Uint8Array, isSigned: boolean): Uint8Array { - if (isSigned) { - return keccak256(message.slice(0, message.length - SignatureLength)); - } - return keccak256(message); -} - -function ecRecoverPubKey( - messageHash: Uint8Array, - signature: Uint8Array -): Uint8Array | undefined { - const recoveryDataView = new DataView(signature.slice(64).buffer); - const recovery = recoveryDataView.getUint8(0); - const _signature = secp.Signature.fromCompact(signature.slice(0, 64)); - - return secp.recoverPublicKey(messageHash, _signature, recovery, false); -} - -/** - * Prepare the payload pre-encryption. - * - * @internal - * @returns The encoded payload, ready for encryption using {@link encryptAsymmetric} - * or {@link encryptSymmetric}. - */ -export async function preCipher( - messagePayload: Uint8Array, - sigPrivKey?: Uint8Array -): Promise { - let envelope = new Uint8Array([0]); // No flags - envelope = addPayloadSizeField(envelope, messagePayload); - envelope = concat([envelope, messagePayload]); - - // Calculate padding: - let rawSize = - FlagsLength + - computeSizeOfPayloadSizeField(messagePayload) + - messagePayload.length; - - if (sigPrivKey) { - rawSize += SignatureLength; - } - - const remainder = rawSize % PaddingTarget; - const paddingSize = PaddingTarget - remainder; - const pad = randomBytes(paddingSize); - - if (!validateDataIntegrity(pad, paddingSize)) { - throw new Error("failed to generate random padding of size " + paddingSize); - } - - envelope = concat([envelope, pad]); - if (sigPrivKey) { - envelope[0] |= IsSignedMask; - const hash = keccak256(envelope); - const bytesSignature = await sign(hash, sigPrivKey); - envelope = concat([envelope, bytesSignature]); - } - - return envelope; -} - -/** - * Decode a decrypted payload. - * - * @internal - */ -export function postCipher( - message: Uint8Array -): { payload: Uint8Array; sig?: Signature } | undefined { - const sizeOfPayloadSizeField = getSizeOfPayloadSizeField(message); - if (sizeOfPayloadSizeField === 0) return; - - const payloadSize = getPayloadSize(message, sizeOfPayloadSizeField); - const payloadStart = 1 + sizeOfPayloadSizeField; - const payload = message.slice(payloadStart, payloadStart + payloadSize); - - const isSigned = isMessageSigned(message); - - let sig; - if (isSigned) { - const signature = getSignature(message); - const hash = getHash(message, isSigned); - const publicKey = ecRecoverPubKey(hash, signature); - sig = { signature, publicKey }; - } - - return { payload, sig }; -} diff --git a/packages/message-encryption/src/symmetric.spec.ts b/packages/message-encryption/src/symmetric.spec.ts new file mode 100644 index 0000000000..47219109fa --- /dev/null +++ b/packages/message-encryption/src/symmetric.spec.ts @@ -0,0 +1,62 @@ +import { expect } from "chai"; +import fc from "fast-check"; + +import { getPublicKey } from "./crypto/index.js"; +import { createDecoder, createEncoder } from "./symmetric.js"; + +const TestContentTopic = "/test/1/waku-message/utf8"; + +describe("Symmetric Encryption", function () { + it("Round trip binary encryption [symmetric, no signature]", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (payload, symKey) => { + const encoder = createEncoder(TestContentTopic, symKey); + const bytes = await encoder.toWire({ payload }); + + const decoder = createDecoder(TestContentTopic, symKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(protoResult); + if (!result) throw "Failed to decode"; + + expect(result.contentTopic).to.equal(TestContentTopic); + expect(result.version).to.equal(1); + expect(result?.payload).to.deep.equal(payload); + expect(result.signature).to.be.undefined; + expect(result.signaturePublicKey).to.be.undefined; + } + ) + ); + }); + + it("Round trip binary encryption [symmetric, signature]", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + fc.uint8Array({ min: 1, minLength: 32, maxLength: 32 }), + async (payload, sigPrivKey, symKey) => { + const sigPubKey = getPublicKey(sigPrivKey); + + const encoder = createEncoder(TestContentTopic, symKey, sigPrivKey); + const bytes = await encoder.toWire({ payload }); + + const decoder = createDecoder(TestContentTopic, symKey); + const protoResult = await decoder.fromWireToProtoObj(bytes!); + if (!protoResult) throw "Failed to proto decode"; + const result = await decoder.fromProtoObj(protoResult); + if (!result) throw "Failed to decode"; + + expect(result.contentTopic).to.equal(TestContentTopic); + expect(result.version).to.equal(1); + expect(result?.payload).to.deep.equal(payload); + expect(result.signature).to.not.be.undefined; + expect(result.signaturePublicKey).to.deep.eq(sigPubKey); + } + ) + ); + }); +}); diff --git a/packages/message-encryption/src/symmetric.ts b/packages/message-encryption/src/symmetric.ts index 8f289324cc..794cf47f4f 100644 --- a/packages/message-encryption/src/symmetric.ts +++ b/packages/message-encryption/src/symmetric.ts @@ -1,32 +1,169 @@ -import { Symmetric } from "./constants.js"; -import { getSubtle, randomBytes } from "./crypto.js"; +import { + Decoder as DecoderV0, + proto, +} from "@waku/core/lib/waku_message/version_0"; +import type { + Decoder as IDecoder, + Encoder as IEncoder, + Message, + ProtoMessage, +} from "@waku/interfaces"; +import debug from "debug"; -export async function encrypt( - iv: Uint8Array, - key: Uint8Array, - clearText: Uint8Array -): Promise { - return getSubtle() - .importKey("raw", key, Symmetric.algorithm, false, ["encrypt"]) - .then((cryptoKey) => - getSubtle().encrypt({ iv, ...Symmetric.algorithm }, cryptoKey, clearText) - ) - .then((cipher) => new Uint8Array(cipher)); +import { + decryptSymmetric, + encryptSymmetric, + postCipher, + preCipher, +} from "./waku_payload.js"; + +import { + DecodedMessage, + generateSymmetricKey, + OneMillion, + Version, +} from "./index.js"; + +export { DecodedMessage, generateSymmetricKey }; + +const log = debug("waku:message-encryption:symmetric"); + +class Encoder implements IEncoder { + constructor( + public contentTopic: string, + private symKey: Uint8Array, + private sigPrivKey?: Uint8Array, + public ephemeral: boolean = false + ) {} + + async toWire(message: Message): Promise { + const protoMessage = await this.toProtoObj(message); + if (!protoMessage) return; + + return proto.WakuMessage.encode(protoMessage); + } + + async toProtoObj(message: Message): Promise { + const timestamp = message.timestamp ?? new Date(); + if (!message.payload) { + log("No payload to encrypt, skipping: ", message); + return; + } + const preparedPayload = await preCipher(message.payload, this.sigPrivKey); + + const payload = await encryptSymmetric(preparedPayload, this.symKey); + return { + payload, + version: Version, + contentTopic: this.contentTopic, + timestamp: BigInt(timestamp.valueOf()) * OneMillion, + rateLimitProof: message.rateLimitProof, + ephemeral: this.ephemeral, + }; + } } -export async function decrypt( - iv: Uint8Array, - key: Uint8Array, - cipherText: Uint8Array -): Promise { - return getSubtle() - .importKey("raw", key, Symmetric.algorithm, false, ["decrypt"]) - .then((cryptoKey) => - getSubtle().decrypt({ iv, ...Symmetric.algorithm }, cryptoKey, cipherText) - ) - .then((clear) => new Uint8Array(clear)); +/** + * Creates an encoder that encrypts messages using symmetric encryption for the + * given key, as defined in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/). + * + * An encoder is used to encode messages in the [`14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/) + * format to be sent over the Waku network. The resulting encoder can then be + * pass to { @link @waku/interfaces.LightPush.push } or + * { @link @waku/interfaces.Relay.send } to automatically encrypt + * and encode outgoing messages. + * + * The payload can optionally be signed with the given private key as defined + * in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/). + * + * @param contentTopic The content topic to set on outgoing messages. + * @param symKey The symmetric key to encrypt the payload with. + * @param sigPrivKey An optional private key to used to sign the payload before encryption. + * @param ephemeral An optional flag to mark message as ephemeral, ie, not to be stored by Waku Store nodes. + */ +export function createEncoder( + contentTopic: string, + symKey: Uint8Array, + sigPrivKey?: Uint8Array, + ephemeral = false +): Encoder { + return new Encoder(contentTopic, symKey, sigPrivKey, ephemeral); } -export function generateIv(): Uint8Array { - return randomBytes(Symmetric.ivSize); +class Decoder extends DecoderV0 implements IDecoder { + constructor(contentTopic: string, private symKey: Uint8Array) { + super(contentTopic); + } + + async fromProtoObj( + protoMessage: ProtoMessage + ): Promise { + const cipherPayload = protoMessage.payload; + + if (protoMessage.version !== Version) { + log( + "Failed to decrypt due to incorrect version, expected:", + Version, + ", actual:", + protoMessage.version + ); + return; + } + + let payload; + if (!cipherPayload) { + log(`No payload to decrypt for contentTopic ${this.contentTopic}`); + return; + } + + try { + payload = await decryptSymmetric(cipherPayload, this.symKey); + } catch (e) { + log( + `Failed to decrypt message using asymmetric decryption for contentTopic: ${this.contentTopic}`, + e + ); + return; + } + + if (!payload) { + log(`Failed to decrypt payload for contentTopic ${this.contentTopic}`); + return; + } + + const res = await postCipher(payload); + + if (!res) { + log(`Failed to decode payload for contentTopic ${this.contentTopic}`); + return; + } + + log("Message decrypted", protoMessage); + return new DecodedMessage( + protoMessage, + res.payload, + res.sig?.signature, + res.sig?.publicKey + ); + } +} + +/** + * Creates a decoder that decrypts messages using symmetric encryption, using + * the given key as defined in [26/WAKU2-PAYLOAD](https://rfc.vac.dev/spec/26/). + * + * A decoder is used to decode messages from the [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/) + * format when received from the Waku network. The resulting decoder can then be + * pass to { @link @waku/interfaces.Filter.subscribe } or + * { @link @waku/interfaces.Relay.subscribe } to automatically decrypt and + * decode incoming messages. + * + * @param contentTopic The resulting decoder will only decode messages with this content topic. + * @param symKey The symmetric key used to decrypt the message. + */ +export function createDecoder( + contentTopic: string, + symKey: Uint8Array +): Decoder { + return new Decoder(contentTopic, symKey); } diff --git a/packages/message-encryption/src/waku_payload.spec.ts b/packages/message-encryption/src/waku_payload.spec.ts new file mode 100644 index 0000000000..4fa1cd2e35 --- /dev/null +++ b/packages/message-encryption/src/waku_payload.spec.ts @@ -0,0 +1,84 @@ +import { expect } from "chai"; +import fc from "fast-check"; + +import { getPublicKey } from "./crypto/index.js"; +import { + decryptAsymmetric, + decryptSymmetric, + encryptAsymmetric, + encryptSymmetric, + postCipher, + preCipher, +} from "./waku_payload.js"; + +describe("Waku Payload", () => { + it("Asymmetric encrypt & decrypt", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array({ minLength: 1 }), + fc.uint8Array({ min: 1, 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); + } + ) + ); + }); + + 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); + } + ) + ); + }); + + it("pre and post cipher", async function () { + await fc.assert( + fc.asyncProperty(fc.uint8Array(), async (message) => { + const enc = await preCipher(message); + const res = postCipher(enc); + + expect(res?.payload).deep.equal( + message, + "Payload was not encrypted then decrypted correctly" + ); + }) + ); + }); + + it("Sign & Recover", async function () { + await fc.assert( + fc.asyncProperty( + fc.uint8Array(), + fc.uint8Array({ minLength: 32, maxLength: 32 }), + async (message, sigPrivKey) => { + const sigPubKey = getPublicKey(sigPrivKey); + + const enc = await preCipher(message, sigPrivKey); + const res = postCipher(enc); + + expect(res?.payload).deep.equal( + message, + "Payload was not encrypted then decrypted correctly" + ); + expect(res?.sig?.publicKey).deep.equal( + sigPubKey, + "signature Public key was not recovered from encrypted then decrypted signature" + ); + } + ) + ); + }); +}); diff --git a/packages/message-encryption/src/waku_payload.ts b/packages/message-encryption/src/waku_payload.ts new file mode 100644 index 0000000000..08846cc896 --- /dev/null +++ b/packages/message-encryption/src/waku_payload.ts @@ -0,0 +1,239 @@ +import * as secp from "@noble/secp256k1"; +import { concat, hexToBytes } from "@waku/byte-utils"; + +import { Symmetric } from "./constants.js"; +import * as ecies from "./crypto/ecies.js"; +import { keccak256, randomBytes, sign } from "./crypto/index.js"; +import * as symmetric from "./crypto/symmetric.js"; + +import { Signature } from "./index.js"; + +const FlagsLength = 1; +const FlagMask = 3; // 0011 +const IsSignedMask = 4; // 0100 +const PaddingTarget = 256; +const SignatureLength = 65; + +function getSizeOfPayloadSizeField(message: Uint8Array): number { + const messageDataView = new DataView(message.buffer); + return messageDataView.getUint8(0) & FlagMask; +} + +function getPayloadSize( + message: Uint8Array, + sizeOfPayloadSizeField: number +): number { + let payloadSizeBytes = message.slice(1, 1 + sizeOfPayloadSizeField); + // int 32 == 4 bytes + if (sizeOfPayloadSizeField < 4) { + // If less than 4 bytes pad right (Little Endian). + payloadSizeBytes = concat( + [payloadSizeBytes, new Uint8Array(4 - sizeOfPayloadSizeField)], + 4 + ); + } + const payloadSizeDataView = new DataView(payloadSizeBytes.buffer); + return payloadSizeDataView.getInt32(0, true); +} + +function isMessageSigned(message: Uint8Array): boolean { + const messageDataView = new DataView(message.buffer); + return (messageDataView.getUint8(0) & IsSignedMask) == IsSignedMask; +} + +/** + * 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`. + * + * @internal + */ +export async function encryptAsymmetric( + data: Uint8Array, + publicKey: Uint8Array | string +): Promise { + return ecies.encrypt(hexToBytes(publicKey), data); +} + +/** + * Proceed with Asymmetric decryption of the data as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/). + * The returned data is expected to be `flags | payload-length | payload | [signature]`. + * + * @internal + */ +export async function decryptAsymmetric( + payload: Uint8Array, + privKey: Uint8Array +): Promise { + return ecies.decrypt(privKey, 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, + key: Uint8Array | string +): Promise { + const iv = symmetric.generateIv(); + + // Returns `cipher | tag` + const cipher = await symmetric.encrypt(iv, hexToBytes(key), data); + return 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, + key: Uint8Array | string +): Promise { + const ivStart = payload.length - Symmetric.ivSize; + const cipher = payload.slice(0, ivStart); + const iv = payload.slice(ivStart); + + return symmetric.decrypt(iv, hexToBytes(key), cipher); +} + +/** + * Computes the flags & auxiliary-field as per [26/WAKU-PAYLOAD](https://rfc.vac.dev/spec/26/). + */ +function addPayloadSizeField(msg: Uint8Array, payload: Uint8Array): Uint8Array { + const fieldSize = computeSizeOfPayloadSizeField(payload); + let field = new Uint8Array(4); + const fieldDataView = new DataView(field.buffer); + fieldDataView.setUint32(0, payload.length, true); + field = field.slice(0, fieldSize); + msg = concat([msg, field]); + msg[0] |= fieldSize; + return msg; +} + +/** + * Returns the size of the auxiliary-field which in turns contains the payload size + */ +function computeSizeOfPayloadSizeField(payload: Uint8Array): number { + let s = 1; + for (let i = payload.length; i >= 256; i /= 256) { + s++; + } + return s; +} + +function validateDataIntegrity( + value: Uint8Array, + expectedSize: number +): boolean { + if (value.length !== expectedSize) { + return false; + } + + return expectedSize <= 3 || value.findIndex((i) => i !== 0) !== -1; +} + +function getSignature(message: Uint8Array): Uint8Array { + return message.slice(message.length - SignatureLength, message.length); +} + +function getHash(message: Uint8Array, isSigned: boolean): Uint8Array { + if (isSigned) { + return keccak256(message.slice(0, message.length - SignatureLength)); + } + return keccak256(message); +} + +function ecRecoverPubKey( + messageHash: Uint8Array, + signature: Uint8Array +): Uint8Array | undefined { + const recoveryDataView = new DataView(signature.slice(64).buffer); + const recovery = recoveryDataView.getUint8(0); + const _signature = secp.Signature.fromCompact(signature.slice(0, 64)); + + return secp.recoverPublicKey(messageHash, _signature, recovery, false); +} + +/** + * Prepare the payload pre-encryption. + * + * @internal + * @returns The encoded payload, ready for encryption using {@link encryptAsymmetric} + * or {@link encryptSymmetric}. + */ +export async function preCipher( + messagePayload: Uint8Array, + sigPrivKey?: Uint8Array +): Promise { + let envelope = new Uint8Array([0]); // No flags + envelope = addPayloadSizeField(envelope, messagePayload); + envelope = concat([envelope, messagePayload]); + + // Calculate padding: + let rawSize = + FlagsLength + + computeSizeOfPayloadSizeField(messagePayload) + + messagePayload.length; + + if (sigPrivKey) { + rawSize += SignatureLength; + } + + const remainder = rawSize % PaddingTarget; + const paddingSize = PaddingTarget - remainder; + const pad = randomBytes(paddingSize); + + if (!validateDataIntegrity(pad, paddingSize)) { + throw new Error("failed to generate random padding of size " + paddingSize); + } + + envelope = concat([envelope, pad]); + if (sigPrivKey) { + envelope[0] |= IsSignedMask; + const hash = keccak256(envelope); + const bytesSignature = await sign(hash, sigPrivKey); + envelope = concat([envelope, bytesSignature]); + } + + return envelope; +} + +/** + * Decode a decrypted payload. + * + * @internal + */ +export function postCipher( + message: Uint8Array +): { payload: Uint8Array; sig?: Signature } | undefined { + const sizeOfPayloadSizeField = getSizeOfPayloadSizeField(message); + if (sizeOfPayloadSizeField === 0) return; + + const payloadSize = getPayloadSize(message, sizeOfPayloadSizeField); + const payloadStart = 1 + sizeOfPayloadSizeField; + const payload = message.slice(payloadStart, payloadStart + payloadSize); + + const isSigned = isMessageSigned(message); + + let sig; + if (isSigned) { + const signature = getSignature(message); + const hash = getHash(message, isSigned); + const publicKey = ecRecoverPubKey(hash, signature); + sig = { signature, publicKey }; + } + + return { payload, sig }; +} diff --git a/packages/tests/tests/enr.node.spec.ts b/packages/tests/tests/enr.node.spec.ts index 3e85ce269c..517007d44b 100644 --- a/packages/tests/tests/enr.node.spec.ts +++ b/packages/tests/tests/enr.node.spec.ts @@ -1,4 +1,4 @@ -import { waitForRemotePeer } from "@waku/core/lib/wait_for_remote_peer"; +import { waitForRemotePeer } from "@waku/core"; import { createPrivacyNode } from "@waku/create"; import { ENR } from "@waku/enr"; import type { WakuPrivacy } from "@waku/interfaces"; diff --git a/packages/tests/tests/ephemeral.node.spec.ts b/packages/tests/tests/ephemeral.node.spec.ts index 78c3ab80b7..755820017f 100644 --- a/packages/tests/tests/ephemeral.node.spec.ts +++ b/packages/tests/tests/ephemeral.node.spec.ts @@ -1,17 +1,24 @@ import { bytesToUtf8, utf8ToBytes } from "@waku/byte-utils"; -import { waitForRemotePeer } from "@waku/core/lib/wait_for_remote_peer"; -import { DecoderV0, EncoderV0 } from "@waku/core/lib/waku_message/version_0"; -import { createLightNode } from "@waku/create"; -import { DecodedMessage, Protocols, WakuLight } from "@waku/interfaces"; import { - AsymDecoder, - AsymEncoder, + createDecoder, + createEncoder, + DecodedMessage, + waitForRemotePeer, +} from "@waku/core"; +import { createLightNode } from "@waku/create"; +import { Protocols } from "@waku/interfaces"; +import type { WakuLight } from "@waku/interfaces"; +import { + createDecoder as eciesDecoder, + createEncoder as eciesEncoder, generatePrivateKey, - generateSymmetricKey, getPublicKey, - SymDecoder, - SymEncoder, -} from "@waku/message-encryption"; +} from "@waku/message-encryption/ecies"; +import { + generateSymmetricKey, + createDecoder as symDecoder, + createEncoder as symEncoder, +} from "@waku/message-encryption/symmetric"; import { expect } from "chai"; import debug from "debug"; @@ -26,8 +33,8 @@ import { const log = debug("waku:test:ephemeral"); const TestContentTopic = "/test/1/ephemeral/utf8"; -const TestEncoder = new EncoderV0(TestContentTopic); -const TestDecoder = new DecoderV0(TestContentTopic); +const TestEncoder = createEncoder(TestContentTopic); +const TestDecoder = createDecoder(TestContentTopic); describe("Waku Message Ephemeral field", () => { let waku: WakuLight; @@ -75,17 +82,17 @@ describe("Waku Message Ephemeral field", () => { const AsymContentTopic = "/test/1/ephemeral-asym/utf8"; const SymContentTopic = "/test/1/ephemeral-sym/utf8"; - const asymEncoder = new AsymEncoder( + const asymEncoder = eciesEncoder( AsymContentTopic, publicKey, undefined, true ); - const symEncoder = new SymEncoder(SymContentTopic, symKey, undefined, true); - const clearEncoder = new EncoderV0(TestContentTopic, true); + const symEncoder = eciesEncoder(SymContentTopic, symKey, undefined, true); + const clearEncoder = createEncoder(TestContentTopic, true); - const asymDecoder = new AsymDecoder(AsymContentTopic, privateKey); - const symDecoder = new SymDecoder(SymContentTopic, symKey); + const asymDecoder = eciesDecoder(AsymContentTopic, privateKey); + const symDecoder = eciesDecoder(SymContentTopic, symKey); const [waku1, waku2, nimWakuMultiaddr] = await Promise.all([ createLightNode({ @@ -142,7 +149,7 @@ describe("Waku Message Ephemeral field", () => { it("Ephemeral field is preserved - encoder v0", async function () { this.timeout(10000); - const ephemeralEncoder = new EncoderV0(TestContentTopic, true); + const ephemeralEncoder = createEncoder(TestContentTopic, true); const messages: DecodedMessage[] = []; const callback = (msg: DecodedMessage): void => { @@ -182,14 +189,14 @@ describe("Waku Message Ephemeral field", () => { const symKey = generateSymmetricKey(); - const ephemeralEncoder = new SymEncoder( + const ephemeralEncoder = symEncoder( TestContentTopic, symKey, undefined, true ); - const encoder = new SymEncoder(TestContentTopic, symKey); - const decoder = new SymDecoder(TestContentTopic, symKey); + const encoder = symEncoder(TestContentTopic, symKey); + const decoder = symDecoder(TestContentTopic, symKey); const messages: DecodedMessage[] = []; const callback = (msg: DecodedMessage): void => { @@ -230,14 +237,14 @@ describe("Waku Message Ephemeral field", () => { const privKey = generatePrivateKey(); const pubKey = getPublicKey(privKey); - const ephemeralEncoder = new AsymEncoder( + const ephemeralEncoder = eciesEncoder( TestContentTopic, pubKey, undefined, true ); - const encoder = new AsymEncoder(TestContentTopic, pubKey); - const decoder = new AsymDecoder(TestContentTopic, privKey); + const encoder = eciesEncoder(TestContentTopic, pubKey); + const decoder = eciesDecoder(TestContentTopic, privKey); const messages: DecodedMessage[] = []; const callback = (msg: DecodedMessage): void => { diff --git a/packages/tests/tests/filter.node.spec.ts b/packages/tests/tests/filter.node.spec.ts index 823d43a33d..0b97563264 100644 --- a/packages/tests/tests/filter.node.spec.ts +++ b/packages/tests/tests/filter.node.spec.ts @@ -1,6 +1,5 @@ import { bytesToUtf8, utf8ToBytes } from "@waku/byte-utils"; -import { waitForRemotePeer } from "@waku/core/lib/wait_for_remote_peer"; -import { DecoderV0, EncoderV0 } from "@waku/core/lib/waku_message/version_0"; +import { createDecoder, createEncoder, waitForRemotePeer } from "@waku/core"; import { createLightNode } from "@waku/create"; import type { DecodedMessage, WakuLight } from "@waku/interfaces"; import { Protocols } from "@waku/interfaces"; @@ -12,8 +11,8 @@ import { delay, makeLogFileName, NOISE_KEY_1, Nwaku } from "../src/index.js"; const log = debug("waku:test"); const TestContentTopic = "/test/1/waku-filter"; -const TestEncoder = new EncoderV0(TestContentTopic); -const TestDecoder = new DecoderV0(TestContentTopic); +const TestEncoder = createEncoder(TestContentTopic); +const TestDecoder = createDecoder(TestContentTopic); describe("Waku Filter", () => { let waku: WakuLight; diff --git a/packages/tests/tests/light_push.node.spec.ts b/packages/tests/tests/light_push.node.spec.ts index 38b8edca22..2a4f11c81a 100644 --- a/packages/tests/tests/light_push.node.spec.ts +++ b/packages/tests/tests/light_push.node.spec.ts @@ -1,6 +1,5 @@ import { bytesToUtf8, utf8ToBytes } from "@waku/byte-utils"; -import { waitForRemotePeer } from "@waku/core/lib/wait_for_remote_peer"; -import { EncoderV0 } from "@waku/core/lib/waku_message/version_0"; +import { createEncoder, waitForRemotePeer } from "@waku/core"; import { createLightNode } from "@waku/create"; import type { WakuLight } from "@waku/interfaces"; import { Protocols } from "@waku/interfaces"; @@ -18,7 +17,7 @@ import { const log = debug("waku:test:lightpush"); const TestContentTopic = "/test/1/waku-light-push/utf8"; -const TestEncoder = new EncoderV0(TestContentTopic); +const TestEncoder = createEncoder(TestContentTopic); describe("Waku Light Push [node only]", () => { let waku: WakuLight; diff --git a/packages/tests/tests/relay.node.spec.ts b/packages/tests/tests/relay.node.spec.ts index e112941ea6..764813dceb 100644 --- a/packages/tests/tests/relay.node.spec.ts +++ b/packages/tests/tests/relay.node.spec.ts @@ -1,24 +1,26 @@ import { PeerId } from "@libp2p/interface-peer-id"; import { bytesToUtf8, utf8ToBytes } from "@waku/byte-utils"; -import { DefaultPubSubTopic } from "@waku/core"; -import { waitForRemotePeer } from "@waku/core/lib/wait_for_remote_peer"; import { - DecoderV0, - EncoderV0, - MessageV0, -} from "@waku/core/lib/waku_message/version_0"; + createDecoder, + createEncoder, + DecodedMessage, + DefaultPubSubTopic, + waitForRemotePeer, +} from "@waku/core"; import { createPrivacyNode } from "@waku/create"; -import type { DecodedMessage, WakuPrivacy } from "@waku/interfaces"; +import type { WakuPrivacy } from "@waku/interfaces"; import { Protocols } from "@waku/interfaces"; import { - AsymDecoder, - AsymEncoder, + createDecoder as createEciesDecoder, + createEncoder as createEciesEncoder, generatePrivateKey, - generateSymmetricKey, getPublicKey, - SymDecoder, - SymEncoder, -} from "@waku/message-encryption"; +} from "@waku/message-encryption/ecies"; +import { + createDecoder as createSymDecoder, + createEncoder as createSymEncoder, + generateSymmetricKey, +} from "@waku/message-encryption/symmetric"; import { expect } from "chai"; import debug from "debug"; @@ -35,8 +37,8 @@ import { const log = debug("waku:test"); const TestContentTopic = "/test/1/waku-relay/utf8"; -const TestEncoder = new EncoderV0(TestContentTopic); -const TestDecoder = new DecoderV0(TestContentTopic); +const TestEncoder = createEncoder(TestContentTopic); +const TestDecoder = createDecoder(TestContentTopic); describe("Waku Relay [node only]", () => { // Node needed as we don't have a way to connect 2 js waku @@ -142,11 +144,11 @@ describe("Waku Relay [node only]", () => { const fooContentTopic = "foo"; const barContentTopic = "bar"; - const fooEncoder = new EncoderV0(fooContentTopic); - const barEncoder = new EncoderV0(barContentTopic); + const fooEncoder = createEncoder(fooContentTopic); + const barEncoder = createEncoder(barContentTopic); - const fooDecoder = new DecoderV0(fooContentTopic); - const barDecoder = new DecoderV0(barContentTopic); + const fooDecoder = createDecoder(fooContentTopic); + const barDecoder = createDecoder(barContentTopic); const fooMessages: DecodedMessage[] = []; waku2.relay.addObserver(fooDecoder, (msg) => { @@ -191,21 +193,21 @@ describe("Waku Relay [node only]", () => { const symKey = generateSymmetricKey(); const publicKey = getPublicKey(privateKey); - const asymEncoder = new AsymEncoder(asymTopic, publicKey); - const symEncoder = new SymEncoder(symTopic, symKey); + const eciesEncoder = createEciesEncoder(asymTopic, publicKey); + const symEncoder = createSymEncoder(symTopic, symKey); - const asymDecoder = new AsymDecoder(asymTopic, privateKey); - const symDecoder = new SymDecoder(symTopic, symKey); + const eciesDecoder = createEciesDecoder(asymTopic, privateKey); + const symDecoder = createSymDecoder(symTopic, symKey); const msgs: DecodedMessage[] = []; - waku2.relay.addObserver(asymDecoder, (wakuMsg) => { + waku2.relay.addObserver(eciesDecoder, (wakuMsg) => { msgs.push(wakuMsg); }); waku2.relay.addObserver(symDecoder, (wakuMsg) => { msgs.push(wakuMsg); }); - await waku1.relay.send(asymEncoder, { payload: utf8ToBytes(asymText) }); + await waku1.relay.send(eciesEncoder, { payload: utf8ToBytes(asymText) }); await delay(200); await waku1.relay.send(symEncoder, { payload: utf8ToBytes(symText) }); @@ -231,14 +233,14 @@ describe("Waku Relay [node only]", () => { const receivedMsgPromise: Promise = new Promise( (resolve, reject) => { const deleteObserver = waku2.relay.addObserver( - new DecoderV0(contentTopic), + createDecoder(contentTopic), reject ); deleteObserver(); setTimeout(resolve, 500); } ); - await waku1.relay.send(new EncoderV0(contentTopic), { + await waku1.relay.send(createEncoder(contentTopic), { payload: utf8ToBytes(messageText), }); @@ -391,9 +393,13 @@ describe("Waku Relay [node only]", () => { const messageText = "Here is another message."; - const receivedMsgPromise: Promise = new Promise((resolve) => { - waku.relay.addObserver(TestDecoder, (msg) => resolve(msg)); - }); + const receivedMsgPromise: Promise = new Promise( + (resolve) => { + waku.relay.addObserver(TestDecoder, (msg) => + resolve(msg) + ); + } + ); await nwaku.sendMessage( Nwaku.toMessageRpcQuery({ @@ -405,7 +411,7 @@ describe("Waku Relay [node only]", () => { const receivedMsg = await receivedMsgPromise; expect(receivedMsg.contentTopic).to.eq(TestContentTopic); - expect(receivedMsg.version).to.eq(0); + expect(receivedMsg.version!).to.eq(0); expect(bytesToUtf8(receivedMsg.payload!)).to.eq(messageText); }); diff --git a/packages/tests/tests/store.node.spec.ts b/packages/tests/tests/store.node.spec.ts index 25b817fa23..5a3ef35735 100644 --- a/packages/tests/tests/store.node.spec.ts +++ b/packages/tests/tests/store.node.spec.ts @@ -1,19 +1,25 @@ import { bytesToUtf8, utf8ToBytes } from "@waku/byte-utils"; -import { createCursor, PageDirection } from "@waku/core"; -import { waitForRemotePeer } from "@waku/core/lib/wait_for_remote_peer"; -import { DecoderV0, EncoderV0 } from "@waku/core/lib/waku_message/version_0"; +import { + createCursor, + createDecoder, + createEncoder, + PageDirection, + waitForRemotePeer, +} from "@waku/core"; import { createLightNode } from "@waku/create"; -import { DecodedMessage, Message, WakuLight } from "@waku/interfaces"; +import type { DecodedMessage, Message, WakuLight } from "@waku/interfaces"; import { Protocols } from "@waku/interfaces"; import { - AsymDecoder, - AsymEncoder, + createDecoder as createEciesDecoder, + createEncoder as createEciesEncoder, generatePrivateKey, - generateSymmetricKey, getPublicKey, - SymDecoder, - SymEncoder, -} from "@waku/message-encryption"; +} from "@waku/message-encryption/ecies"; +import { + createDecoder as createSymDecoder, + createEncoder as createSymEncoder, + generateSymmetricKey, +} from "@waku/message-encryption/symmetric"; import { expect } from "chai"; import debug from "debug"; @@ -28,8 +34,8 @@ import { const log = debug("waku:test:store"); const TestContentTopic = "/test/1/waku-store/utf8"; -const TestEncoder = new EncoderV0(TestContentTopic); -const TestDecoder = new DecoderV0(TestContentTopic); +const TestEncoder = createEncoder(TestContentTopic); +const TestDecoder = createDecoder(TestContentTopic); describe("Waku Store", () => { let waku: WakuLight; @@ -365,16 +371,16 @@ describe("Waku Store", () => { const symKey = generateSymmetricKey(); const publicKey = getPublicKey(privateKey); - const asymEncoder = new AsymEncoder(asymTopic, publicKey); - const symEncoder = new SymEncoder(symTopic, symKey); + const eciesEncoder = createEciesEncoder(asymTopic, publicKey); + const symEncoder = createSymEncoder(symTopic, symKey); - const otherEncoder = new AsymEncoder( + const otherEncoder = createEciesEncoder( TestContentTopic, getPublicKey(generatePrivateKey()) ); - const asymDecoder = new AsymDecoder(asymTopic, privateKey); - const symDecoder = new SymDecoder(symTopic, symKey); + const eciesDecoder = createEciesDecoder(asymTopic, privateKey); + const symDecoder = createSymDecoder(symTopic, symKey); const [waku1, waku2, nimWakuMultiaddr] = await Promise.all([ createLightNode({ @@ -399,7 +405,7 @@ describe("Waku Store", () => { log("Sending messages using light push"); await Promise.all([ - waku1.lightPush.push(asymEncoder, asymMsg), + waku1.lightPush.push(eciesEncoder, asymMsg), waku1.lightPush.push(symEncoder, symMsg), waku1.lightPush.push(otherEncoder, otherMsg), waku1.lightPush.push(TestEncoder, clearMsg), @@ -411,7 +417,7 @@ describe("Waku Store", () => { log("Retrieve messages from store"); for await (const msgPromises of waku2.store.queryGenerator([ - asymDecoder, + eciesDecoder, symDecoder, TestDecoder, ])) { diff --git a/packages/tests/tests/wait_for_remote_peer.node.spec.ts b/packages/tests/tests/wait_for_remote_peer.node.spec.ts index 8adf521525..0fa96d6fff 100644 --- a/packages/tests/tests/wait_for_remote_peer.node.spec.ts +++ b/packages/tests/tests/wait_for_remote_peer.node.spec.ts @@ -1,4 +1,4 @@ -import { waitForRemotePeer } from "@waku/core/lib/wait_for_remote_peer"; +import { waitForRemotePeer } from "@waku/core"; import { createLightNode, createPrivacyNode } from "@waku/create"; import type { WakuLight, WakuPrivacy } from "@waku/interfaces"; import { Protocols } from "@waku/interfaces"; diff --git a/packages/tests/tests/waku.node.spec.ts b/packages/tests/tests/waku.node.spec.ts index 9a85475b7a..5391297c15 100644 --- a/packages/tests/tests/waku.node.spec.ts +++ b/packages/tests/tests/waku.node.spec.ts @@ -1,8 +1,7 @@ import { bootstrap } from "@libp2p/bootstrap"; import type { PeerId } from "@libp2p/interface-peer-id"; import { bytesToUtf8, utf8ToBytes } from "@waku/byte-utils"; -import { DefaultUserAgent } from "@waku/core"; -import { waitForRemotePeer } from "@waku/core/lib/wait_for_remote_peer"; +import { DefaultUserAgent, waitForRemotePeer } from "@waku/core"; import { createLightNode, createPrivacyNode } from "@waku/create"; import type { DecodedMessage, @@ -12,10 +11,10 @@ import type { } from "@waku/interfaces"; import { Protocols } from "@waku/interfaces"; import { + createDecoder, + createEncoder, generateSymmetricKey, - SymDecoder, - SymEncoder, -} from "@waku/message-encryption"; +} from "@waku/message-encryption/symmetric"; import { expect } from "chai"; import { @@ -167,9 +166,9 @@ describe("Decryption Keys", () => { this.timeout(10000); const symKey = generateSymmetricKey(); - const decoder = new SymDecoder(TestContentTopic, symKey); + const decoder = createDecoder(TestContentTopic, symKey); - const encoder = new SymEncoder(TestContentTopic, symKey); + const encoder = createEncoder(TestContentTopic, symKey); const messageText = "Message is encrypted"; const messageTimestamp = new Date("1995-12-17T03:24:00"); const message = {