mirror of https://github.com/waku-org/js-waku.git
Add version 1 support to waku relay, test decryption against nim-waku
This commit is contained in:
parent
acdc032253
commit
f95d9aec3c
|
@ -62,6 +62,7 @@
|
||||||
"rlnrelay",
|
"rlnrelay",
|
||||||
"sandboxed",
|
"sandboxed",
|
||||||
"secio",
|
"secio",
|
||||||
|
"seckey",
|
||||||
"secp",
|
"secp",
|
||||||
"staticnode",
|
"staticnode",
|
||||||
"statusim",
|
"statusim",
|
||||||
|
|
|
@ -1,9 +1,23 @@
|
||||||
import { expect } from 'chai';
|
import { expect } from 'chai';
|
||||||
|
import debug from 'debug';
|
||||||
import fc from 'fast-check';
|
import fc from 'fast-check';
|
||||||
|
import TCP from 'libp2p-tcp';
|
||||||
|
|
||||||
|
import {
|
||||||
|
makeLogFileName,
|
||||||
|
NimWaku,
|
||||||
|
NOISE_KEY_1,
|
||||||
|
WakuRelayMessage,
|
||||||
|
} from '../../test_utils';
|
||||||
|
import { delay } from '../delay';
|
||||||
|
import { hexToBuf } from '../utils';
|
||||||
|
import { Waku } from '../waku';
|
||||||
|
|
||||||
import { getPublicKey } from './version_1';
|
import { getPublicKey } from './version_1';
|
||||||
|
|
||||||
import { WakuMessage } from './index';
|
import { DefaultContentTopic, WakuMessage } from './index';
|
||||||
|
|
||||||
|
const dbg = debug('waku:test:message');
|
||||||
|
|
||||||
describe('Waku Message', function () {
|
describe('Waku Message', function () {
|
||||||
it('Waku message round trip binary serialization [clear]', async function () {
|
it('Waku message round trip binary serialization [clear]', async function () {
|
||||||
|
@ -42,7 +56,7 @@ describe('Waku Message', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
const wireBytes = msg.encode();
|
const wireBytes = msg.encode();
|
||||||
const actual = await WakuMessage.decode(wireBytes, privKey);
|
const actual = await WakuMessage.decode(wireBytes, [privKey]);
|
||||||
|
|
||||||
expect(actual?.payload).to.deep.equal(payload);
|
expect(actual?.payload).to.deep.equal(payload);
|
||||||
}
|
}
|
||||||
|
@ -66,7 +80,7 @@ describe('Waku Message', function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
const wireBytes = msg.encode();
|
const wireBytes = msg.encode();
|
||||||
const actual = await WakuMessage.decode(wireBytes, encPrivKey);
|
const actual = await WakuMessage.decode(wireBytes, [encPrivKey]);
|
||||||
|
|
||||||
expect(actual?.payload).to.deep.equal(payload);
|
expect(actual?.payload).to.deep.equal(payload);
|
||||||
expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey);
|
expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey);
|
||||||
|
@ -75,3 +89,63 @@ describe('Waku Message', function () {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Interop: Nim', function () {
|
||||||
|
let waku: Waku;
|
||||||
|
let nimWaku: NimWaku;
|
||||||
|
|
||||||
|
beforeEach(async function () {
|
||||||
|
this.timeout(30_000);
|
||||||
|
|
||||||
|
waku = await Waku.create({
|
||||||
|
staticNoiseKey: NOISE_KEY_1,
|
||||||
|
libp2p: {
|
||||||
|
addresses: { listen: ['/ip4/0.0.0.0/tcp/0'] },
|
||||||
|
modules: { transport: [TCP] },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const multiAddrWithId = waku.getLocalMultiaddrWithID();
|
||||||
|
nimWaku = new NimWaku(makeLogFileName(this));
|
||||||
|
await nimWaku.start({ staticnode: multiAddrWithId, rpcPrivate: true });
|
||||||
|
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
waku.libp2p.pubsub.once('gossipsub:heartbeat', resolve)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function () {
|
||||||
|
nimWaku ? nimWaku.stop() : null;
|
||||||
|
waku ? await waku.stop() : null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('JS decrypts nim message [asymmetric, no signature]', async function () {
|
||||||
|
this.timeout(10000);
|
||||||
|
await delay(200);
|
||||||
|
|
||||||
|
const messageText = 'Here is an encrypted message.';
|
||||||
|
const message: WakuRelayMessage = {
|
||||||
|
contentTopic: DefaultContentTopic,
|
||||||
|
payload: Buffer.from(messageText, 'utf-8').toString('hex'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const keyPair = await nimWaku.getAsymmetricKeyPair();
|
||||||
|
const privateKey = hexToBuf(keyPair.privateKey);
|
||||||
|
const publicKey = hexToBuf(keyPair.publicKey);
|
||||||
|
|
||||||
|
waku.relay.addDecryptionPrivateKey(privateKey);
|
||||||
|
|
||||||
|
const receivedMsgPromise: Promise<WakuMessage> = new Promise((resolve) => {
|
||||||
|
waku.relay.addObserver(resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
dbg('Post message');
|
||||||
|
await nimWaku.postAsymmetricMessage(message, publicKey);
|
||||||
|
|
||||||
|
const receivedMsg = await receivedMsgPromise;
|
||||||
|
|
||||||
|
expect(receivedMsg.contentTopic).to.eq(message.contentTopic);
|
||||||
|
expect(receivedMsg.version).to.eq(1);
|
||||||
|
expect(receivedMsg.payloadAsUtf8).to.eq(messageText);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
// Ensure that this class matches the proto interface while
|
// Ensure that this class matches the proto interface while
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
|
|
||||||
|
import debug from 'debug';
|
||||||
import { Reader } from 'protobufjs/minimal';
|
import { Reader } from 'protobufjs/minimal';
|
||||||
|
|
||||||
// Protecting the user from protobuf oddities
|
// Protecting the user from protobuf oddities
|
||||||
|
@ -10,6 +11,7 @@ import * as version_1 from './version_1';
|
||||||
|
|
||||||
export const DefaultContentTopic = '/waku/2/default-content/proto';
|
export const DefaultContentTopic = '/waku/2/default-content/proto';
|
||||||
const DefaultVersion = 0;
|
const DefaultVersion = 0;
|
||||||
|
const dbg = debug('waku:message');
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
contentTopic?: string;
|
contentTopic?: string;
|
||||||
|
@ -84,11 +86,11 @@ export class WakuMessage {
|
||||||
*/
|
*/
|
||||||
static async decode(
|
static async decode(
|
||||||
bytes: Uint8Array,
|
bytes: Uint8Array,
|
||||||
decPrivateKey?: Uint8Array
|
decPrivateKeys?: Uint8Array[]
|
||||||
): Promise<WakuMessage | undefined> {
|
): Promise<WakuMessage | undefined> {
|
||||||
const protoBuf = proto.WakuMessage.decode(Reader.create(bytes));
|
const protoBuf = proto.WakuMessage.decode(Reader.create(bytes));
|
||||||
|
|
||||||
return WakuMessage.decodeProto(protoBuf, decPrivateKey);
|
return WakuMessage.decodeProto(protoBuf, decPrivateKeys);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -98,19 +100,52 @@ export class WakuMessage {
|
||||||
*/
|
*/
|
||||||
static async decodeProto(
|
static async decodeProto(
|
||||||
protoBuf: proto.WakuMessage,
|
protoBuf: proto.WakuMessage,
|
||||||
decPrivateKey?: Uint8Array
|
decPrivateKeys?: Uint8Array[]
|
||||||
): Promise<WakuMessage | undefined> {
|
): Promise<WakuMessage | undefined> {
|
||||||
|
if (protoBuf.payload === undefined) {
|
||||||
|
dbg('Payload is undefined');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const payload = protoBuf.payload;
|
||||||
|
|
||||||
let signaturePublicKey;
|
let signaturePublicKey;
|
||||||
let signature;
|
let signature;
|
||||||
if (protoBuf.version === 1 && protoBuf.payload) {
|
if (protoBuf.version === 1 && protoBuf.payload) {
|
||||||
if (!decPrivateKey) return;
|
if (decPrivateKeys === undefined) {
|
||||||
|
dbg('Payload is encrypted but no private keys have been provided.');
|
||||||
|
|
||||||
const dec = await version_1.decryptAsymmetric(
|
return;
|
||||||
protoBuf.payload,
|
}
|
||||||
decPrivateKey
|
|
||||||
|
// Returns a bunch of `undefined` and hopefully one decrypted result
|
||||||
|
const allResults = await Promise.all(
|
||||||
|
decPrivateKeys.map(async (privateKey) => {
|
||||||
|
try {
|
||||||
|
return await version_1.decryptAsymmetric(payload, privateKey);
|
||||||
|
} catch (e) {
|
||||||
|
dbg('Failed to decrypt asymmetric message', e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isDefined = (dec: Uint8Array | undefined): dec is Uint8Array => {
|
||||||
|
return !!dec;
|
||||||
|
};
|
||||||
|
|
||||||
|
const decodedResults = allResults.filter(isDefined);
|
||||||
|
|
||||||
|
if (decodedResults.length === 0) {
|
||||||
|
dbg('Failed to decrypt payload.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const dec = decodedResults[0];
|
||||||
|
|
||||||
const res = await version_1.clearDecode(dec);
|
const res = await version_1.clearDecode(dec);
|
||||||
if (!res) return;
|
if (!res) {
|
||||||
|
dbg('Failed to decode payload.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
Object.assign(protoBuf, { payload: res.payload });
|
Object.assign(protoBuf, { payload: res.payload });
|
||||||
signaturePublicKey = res.sig?.publicKey;
|
signaturePublicKey = res.sig?.publicKey;
|
||||||
signature = res.sig?.signature;
|
signature = res.sig?.signature;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import debug from 'debug';
|
||||||
import Libp2p from 'libp2p';
|
import Libp2p from 'libp2p';
|
||||||
import Gossipsub from 'libp2p-gossipsub';
|
import Gossipsub from 'libp2p-gossipsub';
|
||||||
import { AddrInfo, MessageIdFunction } from 'libp2p-gossipsub/src/interfaces';
|
import { AddrInfo, MessageIdFunction } from 'libp2p-gossipsub/src/interfaces';
|
||||||
|
@ -24,6 +25,8 @@ import { DefaultPubsubTopic, RelayCodec } from './constants';
|
||||||
import { getRelayPeers } from './get_relay_peers';
|
import { getRelayPeers } from './get_relay_peers';
|
||||||
import { RelayHeartbeat } from './relay_heartbeat';
|
import { RelayHeartbeat } from './relay_heartbeat';
|
||||||
|
|
||||||
|
const dbg = debug('waku:relay');
|
||||||
|
|
||||||
export { RelayCodec, DefaultPubsubTopic };
|
export { RelayCodec, DefaultPubsubTopic };
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,9 +63,15 @@ export interface GossipOptions {
|
||||||
export class WakuRelay extends Gossipsub {
|
export class WakuRelay extends Gossipsub {
|
||||||
heartbeat: RelayHeartbeat;
|
heartbeat: RelayHeartbeat;
|
||||||
pubsubTopic: string;
|
pubsubTopic: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decryption private keys to use to attempt decryption of incoming messages.
|
||||||
|
*/
|
||||||
|
public decPrivateKeys: Set<Uint8Array>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* observers called when receiving new message.
|
* observers called when receiving new message.
|
||||||
* Observers under key "" are always called.
|
* Observers under key `""` are always called.
|
||||||
*/
|
*/
|
||||||
public observers: {
|
public observers: {
|
||||||
[contentTopic: string]: Set<(message: WakuMessage) => void>;
|
[contentTopic: string]: Set<(message: WakuMessage) => void>;
|
||||||
|
@ -82,6 +91,7 @@ export class WakuRelay extends Gossipsub {
|
||||||
|
|
||||||
this.heartbeat = new RelayHeartbeat(this);
|
this.heartbeat = new RelayHeartbeat(this);
|
||||||
this.observers = {};
|
this.observers = {};
|
||||||
|
this.decPrivateKeys = new Set();
|
||||||
|
|
||||||
const multicodecs = [constants.RelayCodec];
|
const multicodecs = [constants.RelayCodec];
|
||||||
|
|
||||||
|
@ -113,12 +123,29 @@ export class WakuRelay extends Gossipsub {
|
||||||
await super.publish(this.pubsubTopic, Buffer.from(msg));
|
await super.publish(this.pubsubTopic, Buffer.from(msg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a decryption private key to attempt decryption of messages of
|
||||||
|
* the given content topic.
|
||||||
|
*/
|
||||||
|
addDecryptionPrivateKey(privateKey: Uint8Array): void {
|
||||||
|
this.decPrivateKeys.add(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a decryption private key to attempt decryption of messages of
|
||||||
|
* the given content topic.
|
||||||
|
*/
|
||||||
|
deleteDecryptionPrivateKey(privateKey: Uint8Array): void {
|
||||||
|
this.decPrivateKeys.delete(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register an observer of new messages received via waku relay
|
* Register an observer of new messages received via waku relay
|
||||||
*
|
*
|
||||||
* @param callback called when a new message is received via waku relay
|
* @param callback called when a new message is received via waku relay
|
||||||
* @param contentTopics Content Topics for which the callback with be called,
|
* @param contentTopics Content Topics for which the callback with be called,
|
||||||
* all of them if undefined, [] or ["",..] is passed.
|
* all of them if undefined, [] or ["",..] is passed.
|
||||||
|
* @param decPrivateKeys Private keys used to decrypt incoming Waku Messages.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
addObserver(
|
addObserver(
|
||||||
|
@ -181,22 +208,30 @@ export class WakuRelay extends Gossipsub {
|
||||||
*/
|
*/
|
||||||
subscribe(pubsubTopic: string): void {
|
subscribe(pubsubTopic: string): void {
|
||||||
this.on(pubsubTopic, (event) => {
|
this.on(pubsubTopic, (event) => {
|
||||||
WakuMessage.decode(event.data).then((wakuMsg) => {
|
dbg(`Message received on ${pubsubTopic}`);
|
||||||
if (!wakuMsg) return;
|
WakuMessage.decode(event.data, Array.from(this.decPrivateKeys))
|
||||||
|
.then((wakuMsg) => {
|
||||||
|
if (!wakuMsg) {
|
||||||
|
dbg('Failed to decode Waku Message');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.observers['']) {
|
if (this.observers['']) {
|
||||||
this.observers[''].forEach((callbackFn) => {
|
this.observers[''].forEach((callbackFn) => {
|
||||||
callbackFn(wakuMsg);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (wakuMsg.contentTopic) {
|
|
||||||
if (this.observers[wakuMsg.contentTopic]) {
|
|
||||||
this.observers[wakuMsg.contentTopic].forEach((callbackFn) => {
|
|
||||||
callbackFn(wakuMsg);
|
callbackFn(wakuMsg);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
if (wakuMsg.contentTopic) {
|
||||||
});
|
if (this.observers[wakuMsg.contentTopic]) {
|
||||||
|
this.observers[wakuMsg.contentTopic].forEach((callbackFn) => {
|
||||||
|
callbackFn(wakuMsg);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
dbg('Failed to decode Waku Message', e);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
super.subscribe(pubsubTopic);
|
super.subscribe(pubsubTopic);
|
||||||
|
|
|
@ -41,6 +41,7 @@ export interface Args {
|
||||||
persistMessages?: boolean;
|
persistMessages?: boolean;
|
||||||
lightpush?: boolean;
|
lightpush?: boolean;
|
||||||
topics?: string;
|
topics?: string;
|
||||||
|
rpcPrivate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LogLevel {
|
export enum LogLevel {
|
||||||
|
@ -53,6 +54,16 @@ export enum LogLevel {
|
||||||
Fatal = 'fatal',
|
Fatal = 'fatal',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface KeyPair {
|
||||||
|
privateKey: string;
|
||||||
|
publicKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WakuRelayMessage {
|
||||||
|
payload: string;
|
||||||
|
contentTopic?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class NimWaku {
|
export class NimWaku {
|
||||||
private process?: ChildProcess;
|
private process?: ChildProcess;
|
||||||
private pid?: number;
|
private pid?: number;
|
||||||
|
@ -196,6 +207,35 @@ export class NimWaku {
|
||||||
return msgs.filter(isDefined);
|
return msgs.filter(isDefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getAsymmetricKeyPair(): Promise<KeyPair> {
|
||||||
|
this.checkProcess();
|
||||||
|
|
||||||
|
const { seckey, pubkey } = await this.rpcCall<{
|
||||||
|
seckey: string;
|
||||||
|
pubkey: string;
|
||||||
|
}>('get_waku_v2_private_v1_asymmetric_keypair', []);
|
||||||
|
|
||||||
|
return { privateKey: seckey, publicKey: pubkey };
|
||||||
|
}
|
||||||
|
|
||||||
|
async postAsymmetricMessage(
|
||||||
|
message: WakuRelayMessage,
|
||||||
|
publicKey: Uint8Array,
|
||||||
|
pubsubTopic?: string
|
||||||
|
): Promise<boolean> {
|
||||||
|
this.checkProcess();
|
||||||
|
|
||||||
|
if (!message.payload) {
|
||||||
|
throw 'Attempting to send empty message';
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.rpcCall<boolean>('post_waku_v2_private_v1_asymmetric_message', [
|
||||||
|
pubsubTopic ? pubsubTopic : DefaultPubsubTopic,
|
||||||
|
message,
|
||||||
|
'0x' + bufToHex(publicKey),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
async getPeerId(): Promise<PeerId> {
|
async getPeerId(): Promise<PeerId> {
|
||||||
return await this.setPeerId().then((res) => res.peerId);
|
return await this.setPeerId().then((res) => res.peerId);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue