mirror of
https://github.com/waku-org/js-waku.git
synced 2025-01-12 21:44:33 +00:00
Merge pull request #1049 from waku-org/feat/api-improvements
This commit is contained in:
commit
54397f597a
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -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";
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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");
|
||||
|
33
packages/message-encryption/CHANGELOG.md
Normal file
33
packages/message-encryption/CHANGELOG.md
Normal 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
|
65
packages/message-encryption/README.md
Normal file
65
packages/message-encryption/README.md
Normal 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) => {
|
||||
// ...
|
||||
});
|
||||
```
|
@ -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",
|
||||
|
@ -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",
|
||||
|
194
packages/message-encryption/src/crypto/ecies.ts
Normal file
194
packages/message-encryption/src/crypto/ecies.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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 } = {
|
33
packages/message-encryption/src/crypto/symmetric.ts
Normal file
33
packages/message-encryption/src/crypto/symmetric.ts
Normal 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);
|
||||
}
|
71
packages/message-encryption/src/ecies.spec.ts
Normal file
71
packages/message-encryption/src/ecies.spec.ts
Normal 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);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
@ -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"
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
@ -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 };
|
||||
}
|
||||
|
62
packages/message-encryption/src/symmetric.spec.ts
Normal file
62
packages/message-encryption/src/symmetric.spec.ts
Normal 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);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
||||
|
84
packages/message-encryption/src/waku_payload.spec.ts
Normal file
84
packages/message-encryption/src/waku_payload.spec.ts
Normal 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"
|
||||
);
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
239
packages/message-encryption/src/waku_payload.ts
Normal file
239
packages/message-encryption/src/waku_payload.ts
Normal 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 };
|
||||
}
|
@ -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";
|
||||
|
@ -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 => {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
@ -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,
|
||||
])) {
|
||||
|
@ -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";
|
||||
|
@ -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 = {
|
||||
|
Loading…
x
Reference in New Issue
Block a user