Merge pull request #1049 from waku-org/feat/api-improvements

This commit is contained in:
fryorcraken.eth 2022-12-05 16:41:05 +11:00 committed by GitHub
commit 54397f597a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1328 additions and 993 deletions

View File

@ -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",

View File

@ -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

View File

@ -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"

View File

@ -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",

View File

@ -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";

View File

@ -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

View File

@ -60,7 +60,7 @@ class WakuLightPush implements LightPush {
async push(
encoder: Encoder,
message: Partial<Message>,
message: Message,
opts?: ProtocolOptions
): Promise<SendResult> {
const pubSubTopic = opts?.pubSubTopic ? opts.pubSubTopic : this.pubSubTopic;

View File

@ -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);

View File

@ -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<Message>): Promise<Uint8Array> {
async toWire(message: Message): Promise<Uint8Array> {
return proto.WakuMessage.encode(await this.toProtoObj(message));
}
async toProtoObj(message: Partial<Message>): Promise<ProtoMessage> {
async toProtoObj(message: Message): Promise<ProtoMessage> {
const timestamp = message.timestamp ?? new Date();
return {
@ -92,8 +92,27 @@ export class EncoderV0 implements Encoder {
}
}
export class DecoderV0 implements Decoder<MessageV0> {
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<DecodedMessage> {
constructor(public contentTopic: string) {}
fromWireToProtoObj(bytes: Uint8Array): Promise<ProtoMessage | undefined> {
const protoMessage = proto.WakuMessage.decode(bytes);
@ -108,7 +127,7 @@ export class DecoderV0 implements Decoder<MessageV0> {
});
}
async fromProtoObj(proto: ProtoMessage): Promise<MessageV0 | undefined> {
async fromProtoObj(proto: ProtoMessage): Promise<DecodedMessage | undefined> {
// 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<MessageV0> {
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);
}

View File

@ -99,10 +99,7 @@ class WakuRelay extends GossipSub implements Relay {
/**
* Send Waku message.
*/
public async send(
encoder: Encoder,
message: Partial<Message>
): Promise<SendResult> {
public async send(encoder: Encoder, message: Message): Promise<SendResult> {
const msg = await encoder.toWire(message);
if (!msg) {
log("Failed to encode message, aborting publish");

View File

@ -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

View File

@ -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) => {
// ...
});
```

View File

@ -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",

View File

@ -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",

View File

@ -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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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<boolean> {
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<Uint8Array> {
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<Uint8Array> {
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);
}
}

View File

@ -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<string, any> | undefined;
const crypto: { node?: any; web?: any } = {

View File

@ -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<Uint8Array> {
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<Uint8Array> {
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);
}

View File

@ -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);
}
)
);
});
});

View File

@ -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<Uint8Array> {
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<Uint8Array | undefined> {
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<Uint8Array> {
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<ProtoMessage | undefined> {
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<Uint8Array> {
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<Uint8Array> {
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<boolean> {
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<Uint8Array> {
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<DecodedMessage> {
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<Uint8Array> {
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<DecodedMessage | undefined> {
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);
}

View File

@ -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"
);
}
)
);
});
});

View File

@ -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<Message>): Promise<Uint8Array | undefined> {
const protoMessage = await this.toProtoObj(message);
if (!protoMessage) return;
return proto.WakuMessage.encode(protoMessage);
}
async toProtoObj(
message: Partial<Message>
): Promise<ProtoMessage | undefined> {
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<Message>): Promise<Uint8Array | undefined> {
const protoMessage = await this.toProtoObj(message);
if (!protoMessage) return;
return proto.WakuMessage.encode(protoMessage);
}
async toProtoObj(
message: Partial<Message>
): Promise<ProtoMessage | undefined> {
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<MessageV1> {
constructor(contentTopic: string, private privateKey: Uint8Array) {
super(contentTopic);
}
async fromProtoObj(
protoMessage: ProtoMessage
): Promise<MessageV1 | undefined> {
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<MessageV1> {
constructor(contentTopic: string, private symKey: Uint8Array) {
super(contentTopic);
}
async fromProtoObj(
protoMessage: ProtoMessage
): Promise<MessageV1 | undefined> {
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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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 };
}

View File

@ -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);
}
)
);
});
});

View File

@ -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<Uint8Array> {
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<Uint8Array | undefined> {
const protoMessage = await this.toProtoObj(message);
if (!protoMessage) return;
return proto.WakuMessage.encode(protoMessage);
}
async toProtoObj(message: Message): Promise<ProtoMessage | undefined> {
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<Uint8Array> {
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<DecodedMessage> {
constructor(contentTopic: string, private symKey: Uint8Array) {
super(contentTopic);
}
async fromProtoObj(
protoMessage: ProtoMessage
): Promise<DecodedMessage | undefined> {
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);
}

View File

@ -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"
);
}
)
);
});
});

View File

@ -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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
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 };
}

View File

@ -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";

View File

@ -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 => {

View File

@ -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;

View File

@ -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;

View File

@ -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<DecodedMessage> = 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<MessageV0> = new Promise((resolve) => {
waku.relay.addObserver<MessageV0>(TestDecoder, (msg) => resolve(msg));
});
const receivedMsgPromise: Promise<DecodedMessage> = new Promise(
(resolve) => {
waku.relay.addObserver<DecodedMessage>(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);
});

View File

@ -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,
])) {

View File

@ -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";

View File

@ -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 = {