Skip to content

Commit 6bd93ee

Browse files
authored
Merge pull request #231 from status-im/179-symmetric-encryption-store-relay
2 parents 2715fe0 + 0b3f1a3 commit 6bd93ee

File tree

9 files changed

+170
-53
lines changed

9 files changed

+170
-53
lines changed

karma.conf.js

+12-1
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,21 @@ module.exports = function (config) {
1919
bundlerOptions: {
2020
entrypoints: /.*\.browser\.spec\.ts$/,
2121
},
22-
tsconfig: './tsconfig.karma.json',
2322
coverageOptions: {
2423
instrumentation: false,
2524
},
25+
tsconfig: './tsconfig.json',
26+
compilerOptions: {
27+
noEmit: false,
28+
},
29+
include: {
30+
mode: 'replace',
31+
values: ['src/lib/**/*.ts', 'src/proto/**/*.ts'],
32+
},
33+
exclude: {
34+
mode: 'replace',
35+
values: ['node_modules/**'],
36+
},
2637
},
2738
});
2839
};

src/lib/waku_message/index.spec.ts

+54
Original file line numberDiff line numberDiff line change
@@ -105,5 +105,59 @@ describe('Waku Message: Node only', function () {
105105
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
106106
expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText);
107107
});
108+
109+
it('JS decrypts nim message [symmetric, no signature]', async function () {
110+
this.timeout(10000);
111+
await delay(200);
112+
113+
const messageText = 'Here is a message encrypted in a symmetric manner.';
114+
const message: WakuRelayMessage = {
115+
contentTopic: DefaultContentTopic,
116+
payload: Buffer.from(messageText, 'utf-8').toString('hex'),
117+
};
118+
119+
const symKey = generatePrivateKey();
120+
121+
waku.relay.addDecryptionPrivateKey(symKey);
122+
123+
const receivedMsgPromise: Promise<WakuMessage> = new Promise(
124+
(resolve) => {
125+
waku.relay.addObserver(resolve);
126+
}
127+
);
128+
129+
dbg('Post message');
130+
await nimWaku.postSymmetricMessage(message, symKey);
131+
132+
const receivedMsg = await receivedMsgPromise;
133+
134+
expect(receivedMsg.contentTopic).to.eq(message.contentTopic);
135+
expect(receivedMsg.version).to.eq(1);
136+
expect(receivedMsg.payloadAsUtf8).to.eq(messageText);
137+
});
138+
139+
it('Js encrypts message for nim [symmetric, no signature]', async function () {
140+
this.timeout(5000);
141+
142+
const symKey = await nimWaku.getSymmetricKey();
143+
144+
const messageText =
145+
'This is a message I am going to encrypt with a symmetric key';
146+
const message = await WakuMessage.fromUtf8String(messageText, {
147+
symKey: symKey,
148+
});
149+
150+
await waku.relay.send(message);
151+
152+
let msgs: WakuRelayMessage[] = [];
153+
154+
while (msgs.length === 0) {
155+
await delay(200);
156+
msgs = await nimWaku.getSymmetricMessages(symKey);
157+
}
158+
159+
expect(msgs[0].contentTopic).to.equal(message.contentTopic);
160+
expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText);
161+
});
108162
});
109163
});

src/lib/waku_message/index.ts

+16-8
Original file line numberDiff line numberDiff line change
@@ -117,25 +117,33 @@ export class WakuMessage {
117117
/**
118118
* Decode a byte array into Waku Message.
119119
*
120-
* If the payload is encrypted, then `decPrivateKey` is used for decryption.
120+
* @params bytes The message encoded using protobuf as defined in [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/).
121+
* @params decryptionKeys If the payload is encrypted (version = 1), then the
122+
* keys are used to attempt decryption of the message. The passed key can either
123+
* be asymmetric private keys or symmetric keys, both method are tried for each
124+
* key until the message is decrypted or combinations are ran out.
121125
*/
122126
static async decode(
123127
bytes: Uint8Array,
124-
decPrivateKeys?: Uint8Array[]
128+
decryptionKeys?: Uint8Array[]
125129
): Promise<WakuMessage | undefined> {
126130
const protoBuf = proto.WakuMessage.decode(Reader.create(bytes));
127131

128-
return WakuMessage.decodeProto(protoBuf, decPrivateKeys);
132+
return WakuMessage.decodeProto(protoBuf, decryptionKeys);
129133
}
130134

131135
/**
132-
* Decode a Waku Message Protobuf Object into Waku Message.
136+
* Decode and decrypt Waku Message Protobuf Object into Waku Message.
133137
*
134-
* If the payload is encrypted, then `decPrivateKey` is used for decryption.
138+
* @params protoBuf The message to decode and decrypt.
139+
* @params decryptionKeys If the payload is encrypted (version = 1), then the
140+
* keys are used to attempt decryption of the message. The passed key can either
141+
* be asymmetric private keys or symmetric keys, both method are tried for each
142+
* key until the message is decrypted or combinations are ran out.
135143
*/
136144
static async decodeProto(
137145
protoBuf: proto.WakuMessage,
138-
decPrivateKeys?: Uint8Array[]
146+
decryptionKeys?: Uint8Array[]
139147
): Promise<WakuMessage | undefined> {
140148
if (protoBuf.payload === undefined) {
141149
dbg('Payload is undefined');
@@ -146,14 +154,14 @@ export class WakuMessage {
146154
let signaturePublicKey;
147155
let signature;
148156
if (protoBuf.version === 1 && protoBuf.payload) {
149-
if (decPrivateKeys === undefined) {
157+
if (decryptionKeys === undefined) {
150158
dbg('Payload is encrypted but no private keys have been provided.');
151159
return;
152160
}
153161

154162
// Returns a bunch of `undefined` and hopefully one decrypted result
155163
const allResults = await Promise.all(
156-
decPrivateKeys.map(async (privateKey) => {
164+
decryptionKeys.map(async (privateKey) => {
157165
try {
158166
return await version_1.decryptSymmetric(payload, privateKey);
159167
} catch (e) {

src/lib/waku_message/version_1.ts

+10-10
Original file line numberDiff line numberDiff line change
@@ -172,14 +172,19 @@ export async function decryptSymmetric(
172172
}
173173

174174
/**
175-
* Generate a new private key
175+
* Generate a new key. Can be used as a private key for Asymmetric encryption
176+
* or a key for symmetric encryption.
177+
*
178+
* If using Asymmetric encryption, use {@link getPublicKey} to get the
179+
* corresponding Public Key.
176180
*/
177181
export function generatePrivateKey(): Uint8Array {
178182
return randomBytes(32);
179183
}
180184

181185
/**
182-
* Return the public key for the given private key
186+
* Return the public key for the given private key, to be used for asymmetric
187+
* encryption.
183188
*/
184189
export function getPublicKey(privateKey: Uint8Array | Buffer): Uint8Array {
185190
return secp256k1.publicKeyCreate(privateKey, false);
@@ -217,14 +222,9 @@ function validateDataIntegrity(
217222
return false;
218223
}
219224

220-
if (
221-
expectedSize > 3 &&
222-
Buffer.from(value).equals(Buffer.alloc(value.length))
223-
) {
224-
return false;
225-
}
226-
227-
return true;
225+
return !(
226+
expectedSize > 3 && Buffer.from(value).equals(Buffer.alloc(value.length))
227+
);
228228
}
229229

230230
function getSignature(message: Buffer): Buffer {

src/lib/waku_relay/index.ts

+4-3
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ export interface GossipOptions {
5757
* Implements the [Waku v2 Relay protocol]{@link https://rfc.vac.dev/spec/11/}.
5858
* Must be passed as a `pubsub` module to a {Libp2p} instance.
5959
*
60-
* @implements {Pubsub}
60+
* @implements {require('libp2p-interfaces/src/pubsub')}
6161
* @noInheritDoc
6262
*/
6363
export class WakuRelay extends Gossipsub {
@@ -125,7 +125,9 @@ export class WakuRelay extends Gossipsub {
125125

126126
/**
127127
* Register a decryption private key to attempt decryption of messages of
128-
* the given content topic.
128+
* the given content topic. This can either be a private key for asymmetric
129+
* encryption or a symmetric key. Waku relay will attempt to decrypt messages
130+
* using both methods.
129131
*/
130132
addDecryptionPrivateKey(privateKey: Uint8Array): void {
131133
this.decPrivateKeys.add(privateKey);
@@ -145,7 +147,6 @@ export class WakuRelay extends Gossipsub {
145147
* @param callback called when a new message is received via waku relay
146148
* @param contentTopics Content Topics for which the callback with be called,
147149
* all of them if undefined, [] or ["",..] is passed.
148-
* @param decPrivateKeys Private keys used to decrypt incoming Waku Messages.
149150
* @returns {void}
150151
*/
151152
addObserver(

src/lib/waku_store/index.spec.ts

+29-17
Original file line numberDiff line numberDiff line change
@@ -146,32 +146,42 @@ describe('Waku Store', () => {
146146
expect(result).to.not.eq(-1);
147147
});
148148

149-
it('Retrieves history with asymmetric encrypted messages', async function () {
149+
it('Retrieves history with asymmetric & symmetric encrypted messages', async function () {
150150
this.timeout(10_000);
151151

152152
nimWaku = new NimWaku(makeLogFileName(this));
153153
await nimWaku.start({ persistMessages: true, lightpush: true });
154154

155-
const encryptedMessageText = 'This message is encrypted for me';
155+
const encryptedAsymmetricMessageText =
156+
'This message is encrypted for me using asymmetric';
157+
const encryptedSymmetricMessageText =
158+
'This message is encrypted for me using symmetric encryption';
156159
const clearMessageText =
157160
'This is a clear text message for everyone to read';
158161
const otherEncMessageText =
159162
'This message is not for and I must not be able to read it';
160163

161164
const privateKey = generatePrivateKey();
165+
const symKey = generatePrivateKey();
162166
const publicKey = getPublicKey(privateKey);
163167

164-
const [encryptedMessage, clearMessage, otherEncMessage] = await Promise.all(
165-
[
166-
WakuMessage.fromUtf8String(encryptedMessageText, {
167-
encPublicKey: publicKey,
168-
}),
169-
WakuMessage.fromUtf8String(clearMessageText),
170-
WakuMessage.fromUtf8String(otherEncMessageText, {
171-
encPublicKey: getPublicKey(generatePrivateKey()),
172-
}),
173-
]
174-
);
168+
const [
169+
encryptedAsymmetricMessage,
170+
encryptedSymmetricMessage,
171+
clearMessage,
172+
otherEncMessage,
173+
] = await Promise.all([
174+
WakuMessage.fromUtf8String(encryptedAsymmetricMessageText, {
175+
encPublicKey: publicKey,
176+
}),
177+
WakuMessage.fromUtf8String(encryptedSymmetricMessageText, {
178+
symKey: symKey,
179+
}),
180+
WakuMessage.fromUtf8String(clearMessageText),
181+
WakuMessage.fromUtf8String(otherEncMessageText, {
182+
encPublicKey: getPublicKey(generatePrivateKey()),
183+
}),
184+
]);
175185

176186
dbg('Messages have been encrypted');
177187

@@ -204,7 +214,8 @@ describe('Waku Store', () => {
204214

205215
dbg('Sending messages using light push');
206216
await Promise.all([
207-
await waku1.lightPush.push(encryptedMessage),
217+
waku1.lightPush.push(encryptedAsymmetricMessage),
218+
waku1.lightPush.push(encryptedSymmetricMessage),
208219
waku1.lightPush.push(otherEncMessage),
209220
waku1.lightPush.push(clearMessage),
210221
]);
@@ -218,13 +229,14 @@ describe('Waku Store', () => {
218229
dbg('Retrieve messages from store');
219230
const messages = await waku2.store.queryHistory({
220231
contentTopics: [],
221-
decryptionPrivateKeys: [privateKey],
232+
decryptionKeys: [privateKey, symKey],
222233
});
223234

224-
expect(messages?.length).eq(2);
235+
expect(messages?.length).eq(3);
225236
if (!messages) throw 'Length was tested';
226237
expect(messages[0].payloadAsUtf8).to.eq(clearMessageText);
227-
expect(messages[1].payloadAsUtf8).to.eq(encryptedMessageText);
238+
expect(messages[1].payloadAsUtf8).to.eq(encryptedSymmetricMessageText);
239+
expect(messages[2].payloadAsUtf8).to.eq(encryptedAsymmetricMessageText);
228240

229241
await Promise.all([waku1.stop(), waku2.stop()]);
230242
});

src/lib/waku_store/index.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export interface QueryOptions {
3737
direction?: Direction;
3838
pageSize?: number;
3939
callback?: (messages: WakuMessage[]) => void;
40-
decryptionPrivateKeys?: Uint8Array[];
40+
decryptionKeys?: Uint8Array[];
4141
}
4242

4343
/**
@@ -64,6 +64,9 @@ export class WakuStore {
6464
* @param options.pubsubTopic The pubsub topic to pass to the query. Defaults
6565
* to the value set at creation. See [Waku v2 Topic Usage Recommendations](https://rfc.vac.dev/spec/23/).
6666
* @param options.callback Callback called on page of stored messages as they are retrieved
67+
* @param options.decryptionKeys Keys that will be used to decrypt messages.
68+
* It can be Asymmetric Private Keys and Symmetric Keys in the same array, all keys will be tried with both
69+
* methods.
6770
* @throws If not able to reach the peer to query.
6871
*/
6972
async queryHistory(options: QueryOptions): Promise<WakuMessage[] | null> {
@@ -129,7 +132,7 @@ export class WakuStore {
129132
response.messages.map(async (protoMsg) => {
130133
const msg = await WakuMessage.decodeProto(
131134
protoMsg,
132-
opts.decryptionPrivateKeys
135+
opts.decryptionKeys
133136
);
134137

135138
if (msg) {

src/test_utils/nim_waku.ts

+40
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import debug from 'debug';
1212
import { Multiaddr, multiaddr } from 'multiaddr';
1313
import PeerId from 'peer-id';
1414

15+
import { hexToBuf } from '../lib/utils';
1516
import { WakuMessage } from '../lib/waku_message';
1617
import { DefaultPubsubTopic } from '../lib/waku_relay';
1718
import * as proto from '../proto/waku/v2/message';
@@ -251,6 +252,45 @@ export class NimWaku {
251252
);
252253
}
253254

255+
async getSymmetricKey(): Promise<Buffer> {
256+
this.checkProcess();
257+
258+
return this.rpcCall<string>(
259+
'get_waku_v2_private_v1_symmetric_key',
260+
[]
261+
).then(hexToBuf);
262+
}
263+
264+
async postSymmetricMessage(
265+
message: WakuRelayMessage,
266+
symKey: Uint8Array,
267+
pubsubTopic?: string
268+
): Promise<boolean> {
269+
this.checkProcess();
270+
271+
if (!message.payload) {
272+
throw 'Attempting to send empty message';
273+
}
274+
275+
return this.rpcCall<boolean>('post_waku_v2_private_v1_symmetric_message', [
276+
pubsubTopic ? pubsubTopic : DefaultPubsubTopic,
277+
message,
278+
'0x' + bufToHex(symKey),
279+
]);
280+
}
281+
282+
async getSymmetricMessages(
283+
symKey: Uint8Array,
284+
pubsubTopic?: string
285+
): Promise<WakuRelayMessage[]> {
286+
this.checkProcess();
287+
288+
return await this.rpcCall<WakuRelayMessage[]>(
289+
'get_waku_v2_private_v1_symmetric_messages',
290+
[pubsubTopic ? pubsubTopic : DefaultPubsubTopic, '0x' + bufToHex(symKey)]
291+
);
292+
}
293+
254294
async getPeerId(): Promise<PeerId> {
255295
return await this.setPeerId().then((res) => res.peerId);
256296
}

tsconfig.karma.json

-12
This file was deleted.

0 commit comments

Comments
 (0)