From d12430e19b82a7f50d87f668f20723c84c9f57c2 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 15 Jul 2021 11:10:19 +1000 Subject: [PATCH 1/6] Merge karma tsconfig in karma conf --- karma.conf.js | 13 ++++++++++++- tsconfig.karma.json | 12 ------------ 2 files changed, 12 insertions(+), 13 deletions(-) delete mode 100644 tsconfig.karma.json diff --git a/karma.conf.js b/karma.conf.js index f0b71c49a1..fdceab411f 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -19,10 +19,21 @@ module.exports = function (config) { bundlerOptions: { entrypoints: /.*\.browser\.spec\.ts$/, }, - tsconfig: './tsconfig.karma.json', coverageOptions: { instrumentation: false, }, + tsconfig: './tsconfig.json', + compilerOptions: { + noEmit: false, + }, + include: { + mode: 'replace', + values: ['src/lib/**/*.ts', 'src/proto/**/*.ts'], + }, + exclude: { + mode: 'replace', + values: ['node_modules/**'], + }, }, }); }; diff --git a/tsconfig.karma.json b/tsconfig.karma.json deleted file mode 100644 index f3eb49d1dd..0000000000 --- a/tsconfig.karma.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "extends": "./tsconfig", - "compilerOptions": { - "noEmit": false - }, - "include": [ - "src/lib/**/*.ts", - "src/proto/**/*.ts", - "src/tests/browser/**/*.ts" - ], - "exclude": ["node_modules/**"] -} From 13c8a0527b8ceb1f4c29b4e484788ec9eddc8de2 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 15 Jul 2021 12:12:43 +1000 Subject: [PATCH 2/6] Test symmetric encryption with nim-waku using relay --- src/lib/waku_message/index.spec.ts | 54 ++++++++++++++++++++++++++++++ src/lib/waku_message/version_1.ts | 9 +++-- src/lib/waku_relay/index.ts | 4 ++- src/test_utils/nim_waku.ts | 40 ++++++++++++++++++++++ 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/src/lib/waku_message/index.spec.ts b/src/lib/waku_message/index.spec.ts index a0f14c6ff4..a9f552ab81 100644 --- a/src/lib/waku_message/index.spec.ts +++ b/src/lib/waku_message/index.spec.ts @@ -105,5 +105,59 @@ describe('Waku Message: Node only', function () { expect(msgs[0].contentTopic).to.equal(message.contentTopic); expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText); }); + + it('JS decrypts nim message [symmetric, no signature]', async function () { + this.timeout(10000); + await delay(200); + + const messageText = 'Here is a message encrypted in a symmetric manner.'; + const message: WakuRelayMessage = { + contentTopic: DefaultContentTopic, + payload: Buffer.from(messageText, 'utf-8').toString('hex'), + }; + + const symKey = generatePrivateKey(); + + waku.relay.addDecryptionPrivateKey(symKey); + + const receivedMsgPromise: Promise = new Promise( + (resolve) => { + waku.relay.addObserver(resolve); + } + ); + + dbg('Post message'); + await nimWaku.postSymmetricMessage(message, symKey); + + const receivedMsg = await receivedMsgPromise; + + expect(receivedMsg.contentTopic).to.eq(message.contentTopic); + expect(receivedMsg.version).to.eq(1); + expect(receivedMsg.payloadAsUtf8).to.eq(messageText); + }); + + it('Js encrypts message for nim [symmetric, no signature]', async function () { + this.timeout(5000); + + const symKey = await nimWaku.getSymmetricKey(); + + const messageText = + 'This is a message I am going to encrypt with a symmetric key'; + const message = await WakuMessage.fromUtf8String(messageText, { + symKey: symKey, + }); + + await waku.relay.send(message); + + let msgs: WakuRelayMessage[] = []; + + while (msgs.length === 0) { + await delay(200); + msgs = await nimWaku.getSymmetricMessages(symKey); + } + + expect(msgs[0].contentTopic).to.equal(message.contentTopic); + expect(hexToBuf(msgs[0].payload).toString('utf-8')).to.equal(messageText); + }); }); }); diff --git a/src/lib/waku_message/version_1.ts b/src/lib/waku_message/version_1.ts index 7cdbdf68e8..19ae218ad0 100644 --- a/src/lib/waku_message/version_1.ts +++ b/src/lib/waku_message/version_1.ts @@ -172,14 +172,19 @@ export async function decryptSymmetric( } /** - * Generate a new private key + * Generate a new key. Can be used as a private key for Asymmetric encryption + * or a key for symmetric encryption. + * + * If using Asymmetric encryption, use {@link getPublicKey} to get the + * corresponding Public Key. */ export function generatePrivateKey(): Uint8Array { return randomBytes(32); } /** - * Return the public key for the given private key + * Return the public key for the given private key, to be used for asymmetric + * encryption. */ export function getPublicKey(privateKey: Uint8Array | Buffer): Uint8Array { return secp256k1.publicKeyCreate(privateKey, false); diff --git a/src/lib/waku_relay/index.ts b/src/lib/waku_relay/index.ts index 041a428e35..ac4fc342b5 100644 --- a/src/lib/waku_relay/index.ts +++ b/src/lib/waku_relay/index.ts @@ -125,7 +125,9 @@ export class WakuRelay extends Gossipsub { /** * Register a decryption private key to attempt decryption of messages of - * the given content topic. + * the given content topic. This can either be a private key for asymmetric + * encryption or a symmetric key. Waku relay will attempt to decrypt messages + * using both methods. */ addDecryptionPrivateKey(privateKey: Uint8Array): void { this.decPrivateKeys.add(privateKey); diff --git a/src/test_utils/nim_waku.ts b/src/test_utils/nim_waku.ts index cef77d7c4c..8a9acc0745 100644 --- a/src/test_utils/nim_waku.ts +++ b/src/test_utils/nim_waku.ts @@ -12,6 +12,7 @@ import debug from 'debug'; import { Multiaddr, multiaddr } from 'multiaddr'; import PeerId from 'peer-id'; +import { hexToBuf } from '../lib/utils'; import { WakuMessage } from '../lib/waku_message'; import { DefaultPubsubTopic } from '../lib/waku_relay'; import * as proto from '../proto/waku/v2/message'; @@ -251,6 +252,45 @@ export class NimWaku { ); } + async getSymmetricKey(): Promise { + this.checkProcess(); + + return this.rpcCall( + 'get_waku_v2_private_v1_symmetric_key', + [] + ).then(hexToBuf); + } + + async postSymmetricMessage( + message: WakuRelayMessage, + symKey: Uint8Array, + pubsubTopic?: string + ): Promise { + this.checkProcess(); + + if (!message.payload) { + throw 'Attempting to send empty message'; + } + + return this.rpcCall('post_waku_v2_private_v1_symmetric_message', [ + pubsubTopic ? pubsubTopic : DefaultPubsubTopic, + message, + '0x' + bufToHex(symKey), + ]); + } + + async getSymmetricMessages( + symKey: Uint8Array, + pubsubTopic?: string + ): Promise { + this.checkProcess(); + + return await this.rpcCall( + 'get_waku_v2_private_v1_symmetric_messages', + [pubsubTopic ? pubsubTopic : DefaultPubsubTopic, '0x' + bufToHex(symKey)] + ); + } + async getPeerId(): Promise { return await this.setPeerId().then((res) => res.peerId); } From 75d1b3834c6de41408b69e06c393e15f3bed3d98 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 15 Jul 2021 12:16:03 +1000 Subject: [PATCH 3/6] Simplify expression --- src/lib/waku_message/version_1.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/lib/waku_message/version_1.ts b/src/lib/waku_message/version_1.ts index 19ae218ad0..ca681579ef 100644 --- a/src/lib/waku_message/version_1.ts +++ b/src/lib/waku_message/version_1.ts @@ -222,14 +222,9 @@ function validateDataIntegrity( return false; } - if ( - expectedSize > 3 && - Buffer.from(value).equals(Buffer.alloc(value.length)) - ) { - return false; - } - - return true; + return !( + expectedSize > 3 && Buffer.from(value).equals(Buffer.alloc(value.length)) + ); } function getSignature(message: Buffer): Buffer { From b74acd73dc2b0798ff51b98a82552e39700e75c4 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 15 Jul 2021 12:16:21 +1000 Subject: [PATCH 4/6] Remove unknown type --- src/lib/waku_relay/index.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/waku_relay/index.ts b/src/lib/waku_relay/index.ts index ac4fc342b5..aad4b54fe6 100644 --- a/src/lib/waku_relay/index.ts +++ b/src/lib/waku_relay/index.ts @@ -57,7 +57,7 @@ export interface GossipOptions { * Implements the [Waku v2 Relay protocol]{@link https://rfc.vac.dev/spec/11/}. * Must be passed as a `pubsub` module to a {Libp2p} instance. * - * @implements {Pubsub} + * @implements {require('libp2p-interfaces/src/pubsub')} * @noInheritDoc */ export class WakuRelay extends Gossipsub { @@ -147,7 +147,6 @@ export class WakuRelay extends Gossipsub { * @param callback called when a new message is received via waku relay * @param contentTopics Content Topics for which the callback with be called, * all of them if undefined, [] or ["",..] is passed. - * @param decPrivateKeys Private keys used to decrypt incoming Waku Messages. * @returns {void} */ addObserver( From 44efd28ac1258d464fcb1fb317dff25827f46302 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 15 Jul 2021 12:25:47 +1000 Subject: [PATCH 5/6] Update terminology and docs to cater for both sym and asym encryption --- src/lib/waku_message/index.ts | 24 ++++++++++++++++-------- src/lib/waku_store/index.ts | 7 +++++-- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/lib/waku_message/index.ts b/src/lib/waku_message/index.ts index ea335c171e..5de1e73a7c 100644 --- a/src/lib/waku_message/index.ts +++ b/src/lib/waku_message/index.ts @@ -117,25 +117,33 @@ export class WakuMessage { /** * Decode a byte array into Waku Message. * - * If the payload is encrypted, then `decPrivateKey` is used for decryption. + * @params bytes The message encoded using protobuf as defined in [14/WAKU2-MESSAGE](https://rfc.vac.dev/spec/14/). + * @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. */ static async decode( bytes: Uint8Array, - decPrivateKeys?: Uint8Array[] + decryptionKeys?: Uint8Array[] ): Promise { const protoBuf = proto.WakuMessage.decode(Reader.create(bytes)); - return WakuMessage.decodeProto(protoBuf, decPrivateKeys); + return WakuMessage.decodeProto(protoBuf, decryptionKeys); } /** - * Decode a Waku Message Protobuf Object into Waku Message. + * Decode and decrypt Waku Message Protobuf Object into Waku Message. * - * If the payload is encrypted, then `decPrivateKey` is used for decryption. + * @params protoBuf The message to decode and decrypt. + * @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. */ static async decodeProto( protoBuf: proto.WakuMessage, - decPrivateKeys?: Uint8Array[] + decryptionKeys?: Uint8Array[] ): Promise { if (protoBuf.payload === undefined) { dbg('Payload is undefined'); @@ -146,14 +154,14 @@ export class WakuMessage { let signaturePublicKey; let signature; if (protoBuf.version === 1 && protoBuf.payload) { - if (decPrivateKeys === undefined) { + if (decryptionKeys === undefined) { dbg('Payload is encrypted but no private keys have been provided.'); return; } // Returns a bunch of `undefined` and hopefully one decrypted result const allResults = await Promise.all( - decPrivateKeys.map(async (privateKey) => { + decryptionKeys.map(async (privateKey) => { try { return await version_1.decryptSymmetric(payload, privateKey); } catch (e) { diff --git a/src/lib/waku_store/index.ts b/src/lib/waku_store/index.ts index c1b4762538..0d757daab9 100644 --- a/src/lib/waku_store/index.ts +++ b/src/lib/waku_store/index.ts @@ -37,7 +37,7 @@ export interface QueryOptions { direction?: Direction; pageSize?: number; callback?: (messages: WakuMessage[]) => void; - decryptionPrivateKeys?: Uint8Array[]; + decryptionKeys?: Uint8Array[]; } /** @@ -64,6 +64,9 @@ export class WakuStore { * @param options.pubsubTopic The pubsub topic to pass to the query. Defaults * to the value set at creation. See [Waku v2 Topic Usage Recommendations](https://rfc.vac.dev/spec/23/). * @param options.callback Callback called on page of stored messages as they are retrieved + * @param options.decryptionKeys Keys that will be used to decrypt messages. + * It can be Asymmetric Private Keys and Symmetric Keys in the same array, all keys will be tried with both + * methods. * @throws If not able to reach the peer to query. */ async queryHistory(options: QueryOptions): Promise { @@ -129,7 +132,7 @@ export class WakuStore { response.messages.map(async (protoMsg) => { const msg = await WakuMessage.decodeProto( protoMsg, - opts.decryptionPrivateKeys + opts.decryptionKeys ); if (msg) { From 0b3f1a33c2c8bde455d6071f8b3db57a991f4bc2 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 15 Jul 2021 12:26:05 +1000 Subject: [PATCH 6/6] test: Symmetric encryption with waku store --- src/lib/waku_store/index.spec.ts | 46 ++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/lib/waku_store/index.spec.ts b/src/lib/waku_store/index.spec.ts index d602439ef8..79a6d9436e 100644 --- a/src/lib/waku_store/index.spec.ts +++ b/src/lib/waku_store/index.spec.ts @@ -146,32 +146,42 @@ describe('Waku Store', () => { expect(result).to.not.eq(-1); }); - it('Retrieves history with asymmetric encrypted messages', async function () { + it('Retrieves history with asymmetric & symmetric encrypted messages', async function () { this.timeout(10_000); nimWaku = new NimWaku(makeLogFileName(this)); await nimWaku.start({ persistMessages: true, lightpush: true }); - const encryptedMessageText = 'This message is encrypted for me'; + const encryptedAsymmetricMessageText = + 'This message is encrypted for me using asymmetric'; + const encryptedSymmetricMessageText = + 'This message is encrypted for me using symmetric encryption'; 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 = generatePrivateKey(); const publicKey = getPublicKey(privateKey); - const [encryptedMessage, clearMessage, otherEncMessage] = await Promise.all( - [ - WakuMessage.fromUtf8String(encryptedMessageText, { - encPublicKey: publicKey, - }), - WakuMessage.fromUtf8String(clearMessageText), - WakuMessage.fromUtf8String(otherEncMessageText, { - encPublicKey: getPublicKey(generatePrivateKey()), - }), - ] - ); + const [ + encryptedAsymmetricMessage, + encryptedSymmetricMessage, + clearMessage, + otherEncMessage, + ] = await Promise.all([ + WakuMessage.fromUtf8String(encryptedAsymmetricMessageText, { + encPublicKey: publicKey, + }), + WakuMessage.fromUtf8String(encryptedSymmetricMessageText, { + symKey: symKey, + }), + WakuMessage.fromUtf8String(clearMessageText), + WakuMessage.fromUtf8String(otherEncMessageText, { + encPublicKey: getPublicKey(generatePrivateKey()), + }), + ]); dbg('Messages have been encrypted'); @@ -204,7 +214,8 @@ describe('Waku Store', () => { dbg('Sending messages using light push'); await Promise.all([ - await waku1.lightPush.push(encryptedMessage), + waku1.lightPush.push(encryptedAsymmetricMessage), + waku1.lightPush.push(encryptedSymmetricMessage), waku1.lightPush.push(otherEncMessage), waku1.lightPush.push(clearMessage), ]); @@ -218,13 +229,14 @@ describe('Waku Store', () => { dbg('Retrieve messages from store'); const messages = await waku2.store.queryHistory({ contentTopics: [], - decryptionPrivateKeys: [privateKey], + decryptionKeys: [privateKey, symKey], }); - expect(messages?.length).eq(2); + expect(messages?.length).eq(3); if (!messages) throw 'Length was tested'; expect(messages[0].payloadAsUtf8).to.eq(clearMessageText); - expect(messages[1].payloadAsUtf8).to.eq(encryptedMessageText); + expect(messages[1].payloadAsUtf8).to.eq(encryptedSymmetricMessageText); + expect(messages[2].payloadAsUtf8).to.eq(encryptedAsymmetricMessageText); await Promise.all([waku1.stop(), waku2.stop()]); });