Allow specifying decryption methods and content topic (#409)

This commit is contained in:
Franck R 2022-01-17 14:11:05 +11:00 committed by GitHub
parent f9d066252c
commit 96cf24d34e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 333 additions and 43 deletions

View File

@ -12,6 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Implement DNS Discovery as per [EIP-1459](https://eips.ethereum.org/EIPS/eip-1459),
with ENR records as defined in [31/WAKU2-ENR](https://rfc.vac.dev/spec/31/);
Available by passing `{ bootstrap: { enrUrl: enrtree://... } }` to `Waku.create`.
- When using `addDecryptionKey`,
it is now possible to specify the decryption method and the content topics of the messages to decrypt;
this is to reduce the number of decryption attempt done and improve performance.
### Changed
@ -20,6 +23,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Minimum node version changed to 16.
- **Breaking**: Changed `Waku.create` bootstrap option from `{ bootstrap: boolean }` to `{ bootstrap: BootstrapOptions }`.
Replace `{ boostrap: true }` with `{ boostrap: { default: true } }` to retain same behaviour.
- **Breaking**: `WakuMessage.decode` and `WakuMessage.decodeProto` now accepts method and content topics for the decryption key.
`WakuMessage.decode(bytes, [key])` becomes `WakuMessage.decode(bytes, [{key: key}])`.
### Fixed

View File

@ -18,11 +18,10 @@ import Ping from 'libp2p/src/ping';
import { Multiaddr, multiaddr } from 'multiaddr';
import PeerId from 'peer-id';
import { Bootstrap } from './discovery';
import { BootstrapOptions } from './discovery/bootstrap';
import { Bootstrap, BootstrapOptions } from './discovery';
import { getPeersForProtocol } from './select_peer';
import { LightPushCodec, WakuLightPush } from './waku_light_push';
import { WakuMessage } from './waku_message';
import { DecryptionMethod, WakuMessage } from './waku_message';
import { RelayCodecs, WakuRelay } from './waku_relay';
import { RelayPingContentTopic } from './waku_relay/constants';
import { StoreCodec, WakuStore } from './waku_store';
@ -135,7 +134,9 @@ export class Waku {
this.stopKeepAlive(connection.remotePeer);
});
options?.decryptionKeys?.forEach(this.addDecryptionKey);
options?.decryptionKeys?.forEach((key) => {
this.addDecryptionKey(key);
});
}
/**
@ -270,9 +271,12 @@ export class Waku {
*
* Strings must be in hex format.
*/
addDecryptionKey(key: Uint8Array | string): void {
this.relay.addDecryptionKey(key);
this.store.addDecryptionKey(key);
addDecryptionKey(
key: Uint8Array | string,
options?: { method?: DecryptionMethod; contentTopics?: string[] }
): void {
this.relay.addDecryptionKey(key, options);
this.store.addDecryptionKey(key, options);
}
/**

View File

@ -36,15 +36,15 @@ describe('Waku Message: Browser & Node', function () {
fc.asyncProperty(
fc.uint8Array({ minLength: 1 }),
fc.uint8Array({ minLength: 32, maxLength: 32 }),
async (payload, privKey) => {
const publicKey = getPublicKey(privKey);
async (payload, key) => {
const publicKey = getPublicKey(key);
const msg = await WakuMessage.fromBytes(payload, TestContentTopic, {
encPublicKey: publicKey,
});
const wireBytes = msg.encode();
const actual = await WakuMessage.decode(wireBytes, [privKey]);
const actual = await WakuMessage.decode(wireBytes, [{ key }]);
expect(actual?.payload).to.deep.equal(payload);
}
@ -68,7 +68,9 @@ describe('Waku Message: Browser & Node', function () {
});
const wireBytes = msg.encode();
const actual = await WakuMessage.decode(wireBytes, [encPrivKey]);
const actual = await WakuMessage.decode(wireBytes, [
{ key: encPrivKey },
]);
expect(actual?.payload).to.deep.equal(payload);
expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey);
@ -88,7 +90,7 @@ describe('Waku Message: Browser & Node', function () {
});
const wireBytes = msg.encode();
const actual = await WakuMessage.decode(wireBytes, [key]);
const actual = await WakuMessage.decode(wireBytes, [{ key }]);
expect(actual?.payload).to.deep.equal(payload);
}
@ -111,7 +113,7 @@ describe('Waku Message: Browser & Node', function () {
});
const wireBytes = msg.encode();
const actual = await WakuMessage.decode(wireBytes, [symKey]);
const actual = await WakuMessage.decode(wireBytes, [{ key: symKey }]);
expect(actual?.payload).to.deep.equal(payload);
expect(actual?.signaturePublicKey).to.deep.equal(sigPubKey);

View File

@ -12,6 +12,11 @@ import * as version_1 from './version_1';
const DefaultVersion = 0;
const dbg = debug('waku:message');
export enum DecryptionMethod {
Asymmetric = 'asymmetric',
Symmetric = 'symmetric',
}
export interface Options {
/**
* Timestamp to set on the message, defaults to now if not passed.
@ -44,7 +49,7 @@ export class WakuMessage {
) {}
/**
* Create Message with a utf-8 string as payload.
* Create Message with an utf-8 string as payload.
*/
static async fromUtf8String(
utf8: string,
@ -116,11 +121,15 @@ export class WakuMessage {
* @params decryptionKeys If the payload is encrypted (version = 1), then the
* keys are used to attempt decryption of the message. The passed key can either
* be asymmetric private keys or symmetric keys, both method are tried for each
* key until the message is decrypted or combinations are ran out.
* key until the message is decrypted or combinations are run out.
*/
static async decode(
bytes: Uint8Array,
decryptionKeys?: Uint8Array[]
decryptionKeys?: Array<{
key: Uint8Array;
method?: DecryptionMethod;
contentTopic?: string[];
}>
): Promise<WakuMessage | undefined> {
const protoBuf = proto.WakuMessage.decode(Reader.create(bytes));
@ -134,11 +143,15 @@ export class WakuMessage {
* @params decryptionKeys If the payload is encrypted (version = 1), then the
* keys are used to attempt decryption of the message. The passed key can either
* be asymmetric private keys or symmetric keys, both method are tried for each
* key until the message is decrypted or combinations are ran out.
* key until the message is decrypted or combinations are run out.
*/
static async decodeProto(
protoBuf: proto.WakuMessage,
decryptionKeys?: Uint8Array[]
decryptionKeys?: Array<{
key: Uint8Array;
method?: DecryptionMethod;
contentTopics?: string[];
}>
): Promise<WakuMessage | undefined> {
if (protoBuf.payload === undefined) {
dbg('Payload is undefined');
@ -156,17 +169,55 @@ export class WakuMessage {
// Returns a bunch of `undefined` and hopefully one decrypted result
const allResults = await Promise.all(
decryptionKeys.map(async (privateKey) => {
try {
return await version_1.decryptSymmetric(payload, privateKey);
} catch (e) {
dbg('Failed to decrypt message using symmetric encryption', e);
try {
return await version_1.decryptAsymmetric(payload, privateKey);
} catch (e) {
dbg('Failed to decrypt message using asymmetric encryption', e);
return;
decryptionKeys.map(async ({ key, method, contentTopics }) => {
if (
!contentTopics ||
(protoBuf.contentTopic &&
contentTopics.includes(protoBuf.contentTopic))
) {
switch (method) {
case DecryptionMethod.Asymmetric:
try {
return await version_1.decryptAsymmetric(payload, key);
} catch (e) {
dbg(
'Failed to decrypt message using symmetric encryption despite decryption method being specified',
e
);
return;
}
case DecryptionMethod.Symmetric:
try {
return await version_1.decryptSymmetric(payload, key);
} catch (e) {
dbg(
'Failed to decrypt message using asymmetric encryption despite decryption method being specified',
e
);
return;
}
default:
try {
return await version_1.decryptSymmetric(payload, key);
} catch (e) {
dbg(
'Failed to decrypt message using symmetric encryption',
e
);
try {
return await version_1.decryptAsymmetric(payload, key);
} catch (e) {
dbg(
'Failed to decrypt message using asymmetric encryption',
e
);
return;
}
}
}
} else {
// No key available for this content topic
return;
}
})
);

View File

@ -12,7 +12,12 @@ import {
} from '../../test_utils';
import { delay } from '../delay';
import { DefaultPubSubTopic, Waku } from '../waku';
import { WakuMessage } from '../waku_message';
import { DecryptionMethod, WakuMessage } from '../waku_message';
import {
generatePrivateKey,
generateSymmetricKey,
getPublicKey,
} from '../waku_message/version_1';
const log = debug('waku:test');
@ -158,6 +163,72 @@ describe('Waku Relay [node only]', () => {
expect(allMessages[1].payloadAsUtf8).to.eq(barMessageText);
});
it('Decrypt messages', async function () {
this.timeout(10000);
const encryptedAsymmetricMessageText =
'This message is encrypted using asymmetric';
const encryptedAsymmetricContentTopic = '/test/1/asymmetric/proto';
const encryptedSymmetricMessageText =
'This message is encrypted using symmetric encryption';
const encryptedSymmetricContentTopic = '/test/1/symmetric/proto';
const privateKey = generatePrivateKey();
const symKey = generateSymmetricKey();
const publicKey = getPublicKey(privateKey);
const [encryptedAsymmetricMessage, encryptedSymmetricMessage] =
await Promise.all([
WakuMessage.fromUtf8String(
encryptedAsymmetricMessageText,
encryptedAsymmetricContentTopic,
{
encPublicKey: publicKey,
}
),
WakuMessage.fromUtf8String(
encryptedSymmetricMessageText,
encryptedSymmetricContentTopic,
{
symKey: symKey,
}
),
]);
waku2.addDecryptionKey(privateKey, {
contentTopics: [encryptedAsymmetricContentTopic],
method: DecryptionMethod.Asymmetric,
});
waku2.addDecryptionKey(symKey, {
contentTopics: [encryptedSymmetricContentTopic],
method: DecryptionMethod.Symmetric,
});
const msgs: WakuMessage[] = [];
waku2.relay.addObserver((wakuMsg) => {
msgs.push(wakuMsg);
});
await waku1.relay.send(encryptedAsymmetricMessage);
await waku1.relay.send(encryptedSymmetricMessage);
while (msgs.length < 2) {
await delay(200);
}
expect(msgs.length).to.eq(2);
expect(msgs[0].contentTopic).to.eq(
encryptedAsymmetricMessage.contentTopic
);
expect(msgs[0].version).to.eq(encryptedAsymmetricMessage.version);
expect(msgs[0].payloadAsUtf8).to.eq(encryptedAsymmetricMessageText);
expect(msgs[1].contentTopic).to.eq(
encryptedSymmetricMessage.contentTopic
);
expect(msgs[1].version).to.eq(encryptedSymmetricMessage.version);
expect(msgs[1].payloadAsUtf8).to.eq(encryptedSymmetricMessageText);
});
it('Delete observer', async function () {
this.timeout(10000);

View File

@ -19,7 +19,7 @@ import PeerId from 'peer-id';
import { hexToBuf } from '../utils';
import { CreateOptions, DefaultPubSubTopic } from '../waku';
import { WakuMessage } from '../waku_message';
import { DecryptionMethod, WakuMessage } from '../waku_message';
import * as constants from './constants';
import { RelayCodecs } from './constants';
@ -65,7 +65,10 @@ export class WakuRelay extends Gossipsub {
heartbeat: RelayHeartbeat;
pubSubTopic: string;
public decryptionKeys: Set<Uint8Array>;
public decryptionKeys: Map<
Uint8Array,
{ method?: DecryptionMethod; contentTopics?: string[] }
>;
/**
* observers called when receiving new message.
@ -89,13 +92,17 @@ export class WakuRelay extends Gossipsub {
this.heartbeat = new RelayHeartbeat(this);
this.observers = {};
this.decryptionKeys = new Set();
this.decryptionKeys = new Map();
const multicodecs = constants.RelayCodecs;
Object.assign(this, { multicodecs });
this.pubSubTopic = options?.pubSubTopic || DefaultPubSubTopic;
options?.decryptionKeys?.forEach((key) => {
this.addDecryptionKey(key);
});
}
/**
@ -128,8 +135,11 @@ export class WakuRelay extends Gossipsub {
*
* Strings must be in hex format.
*/
addDecryptionKey(key: Uint8Array | string): void {
this.decryptionKeys.add(hexToBuf(key));
addDecryptionKey(
key: Uint8Array | string,
options?: { method?: DecryptionMethod; contentTopics?: string[] }
): void {
this.decryptionKeys.set(hexToBuf(key), options ?? {});
}
/**
@ -210,8 +220,18 @@ export class WakuRelay extends Gossipsub {
*/
subscribe(pubSubTopic: string): void {
this.on(pubSubTopic, (event) => {
const decryptionKeys = Array.from(this.decryptionKeys).map(
([key, { method, contentTopics }]) => {
return {
key,
method,
contentTopics,
};
}
);
dbg(`Message received on ${pubSubTopic}`);
WakuMessage.decode(event.data, Array.from(this.decryptionKeys))
WakuMessage.decode(event.data, decryptionKeys)
.then((wakuMsg) => {
if (!wakuMsg) {
dbg('Failed to decode Waku Message');

View File

@ -12,7 +12,7 @@ import {
} from '../../test_utils';
import { delay } from '../delay';
import { Waku } from '../waku';
import { WakuMessage } from '../waku_message';
import { DecryptionMethod, WakuMessage } from '../waku_message';
import {
generatePrivateKey,
generateSymmetricKey,
@ -335,6 +335,122 @@ describe('Waku Store', () => {
await Promise.all([waku1.stop(), waku2.stop()]);
});
it('Retrieves history with asymmetric & symmetric encrypted messages on different content topics', async function () {
this.timeout(10_000);
nimWaku = new NimWaku(makeLogFileName(this));
await nimWaku.start({ persistMessages: true, lightpush: true });
const encryptedAsymmetricMessageText =
'This message is encrypted for me using asymmetric';
const encryptedAsymmetricContentTopic = '/test/1/asymmetric/proto';
const encryptedSymmetricMessageText =
'This message is encrypted for me using symmetric encryption';
const encryptedSymmetricContentTopic = '/test/1/symmetric/proto';
const clearMessageText =
'This is a clear text message for everyone to read';
const otherEncMessageText =
'This message is not for and I must not be able to read it';
const privateKey = generatePrivateKey();
const symKey = generateSymmetricKey();
const publicKey = getPublicKey(privateKey);
const [
encryptedAsymmetricMessage,
encryptedSymmetricMessage,
clearMessage,
otherEncMessage,
] = await Promise.all([
WakuMessage.fromUtf8String(
encryptedAsymmetricMessageText,
encryptedAsymmetricContentTopic,
{
encPublicKey: publicKey,
}
),
WakuMessage.fromUtf8String(
encryptedSymmetricMessageText,
encryptedSymmetricContentTopic,
{
symKey: symKey,
}
),
WakuMessage.fromUtf8String(
clearMessageText,
encryptedAsymmetricContentTopic
),
WakuMessage.fromUtf8String(
otherEncMessageText,
encryptedSymmetricContentTopic,
{
encPublicKey: getPublicKey(generatePrivateKey()),
}
),
]);
dbg('Messages have been encrypted');
const [waku1, waku2, nimWakuMultiaddr] = await Promise.all([
Waku.create({
staticNoiseKey: NOISE_KEY_1,
libp2p: { modules: { transport: [TCP] } },
}),
Waku.create({
staticNoiseKey: NOISE_KEY_2,
libp2p: { modules: { transport: [TCP] } },
}),
nimWaku.getMultiaddrWithId(),
]);
dbg('Waku nodes created');
await Promise.all([
waku1.dial(nimWakuMultiaddr),
waku2.dial(nimWakuMultiaddr),
]);
dbg('Waku nodes connected to nim Waku');
let lightPushPeers = waku1.lightPush.peers;
while (lightPushPeers.length == 0) {
await delay(100);
lightPushPeers = waku1.lightPush.peers;
}
dbg('Sending messages using light push');
await Promise.all([
waku1.lightPush.push(encryptedAsymmetricMessage),
waku1.lightPush.push(encryptedSymmetricMessage),
waku1.lightPush.push(otherEncMessage),
waku1.lightPush.push(clearMessage),
]);
let storePeers = waku2.store.peers;
while (storePeers.length == 0) {
await delay(100);
storePeers = waku2.store.peers;
}
waku2.addDecryptionKey(symKey, {
contentTopics: [encryptedSymmetricContentTopic],
method: DecryptionMethod.Symmetric,
});
dbg('Retrieve messages from store');
const messages = await waku2.store.queryHistory([], {
decryptionKeys: [privateKey],
});
expect(messages?.length).eq(3);
if (!messages) throw 'Length was tested';
expect(messages[0].payloadAsUtf8).to.eq(clearMessageText);
expect(messages[1].payloadAsUtf8).to.eq(encryptedSymmetricMessageText);
expect(messages[2].payloadAsUtf8).to.eq(encryptedAsymmetricMessageText);
await Promise.all([waku1.stop(), waku2.stop()]);
});
it('Retrieves history using start and end time', async function () {
this.timeout(5_000);

View File

@ -10,7 +10,7 @@ import { HistoryResponse_Error } from '../../proto';
import { getPeersForProtocol, selectRandomPeer } from '../select_peer';
import { hexToBuf } from '../utils';
import { DefaultPubSubTopic } from '../waku';
import { WakuMessage } from '../waku_message';
import { DecryptionMethod, WakuMessage } from '../waku_message';
import { HistoryRPC, PageDirection } from './history_rpc';
@ -96,7 +96,10 @@ export interface QueryOptions {
*/
export class WakuStore {
pubSubTopic: string;
public decryptionKeys: Set<Uint8Array>;
public decryptionKeys: Map<
Uint8Array,
{ method?: DecryptionMethod; contentTopics?: string[] }
>;
constructor(public libp2p: Libp2p, options?: CreateOptions) {
if (options?.pubSubTopic) {
@ -105,7 +108,7 @@ export class WakuStore {
this.pubSubTopic = DefaultPubSubTopic;
}
this.decryptionKeys = new Set();
this.decryptionKeys = new Map();
}
/**
@ -157,10 +160,25 @@ export class WakuStore {
const connection = this.libp2p.connectionManager.get(peer.id);
if (!connection) throw 'Failed to get a connection to the peer';
const decryptionKeys = Array.from(this.decryptionKeys.values());
const decryptionKeys = Array.from(this.decryptionKeys).map(
([key, { method, contentTopics }]) => {
return {
key,
method,
contentTopics,
};
}
);
// Add the decryption keys passed to this function against the
// content topics also passed to this function.
if (opts.decryptionKeys) {
opts.decryptionKeys.forEach((key) => {
decryptionKeys.push(hexToBuf(key));
decryptionKeys.push({
key: hexToBuf(key),
contentTopics: contentTopics.length ? contentTopics : undefined,
method: undefined,
});
});
}
@ -248,8 +266,11 @@ export class WakuStore {
*
* Strings must be in hex format.
*/
addDecryptionKey(key: Uint8Array | string): void {
this.decryptionKeys.add(hexToBuf(key));
addDecryptionKey(
key: Uint8Array | string,
options?: { method?: DecryptionMethod; contentTopics?: string[] }
): void {
this.decryptionKeys.set(hexToBuf(key), options ?? {});
}
/**