From 8f702928bc1216718f934d376354cd5e6e400e08 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 23 Sep 2021 15:42:15 +1000 Subject: [PATCH 1/3] Add encryption for chat groups --- packages/status-communities/package.json | 1 + packages/status-communities/src/chat.ts | 16 +++++++++++++--- packages/status-communities/src/encryption.ts | 9 +++++++++ .../status-communities/src/messenger.spec.ts | 4 ++-- packages/status-communities/src/messenger.ts | 13 ++++++++----- yarn.lock | 3 ++- 6 files changed, 35 insertions(+), 11 deletions(-) create mode 100644 packages/status-communities/src/encryption.ts diff --git a/packages/status-communities/package.json b/packages/status-communities/package.json index cf5a438..09a82cd 100644 --- a/packages/status-communities/package.json +++ b/packages/status-communities/package.json @@ -44,6 +44,7 @@ }, "dependencies": { "buffer": "^6.0.3", + "ecies-geth": "^1.5.3", "js-sha3": "^0.8.0", "js-waku": "^0.12.0", "protobufjs": "^6.11.2", diff --git a/packages/status-communities/src/chat.ts b/packages/status-communities/src/chat.ts index 2de593a..20abeb1 100644 --- a/packages/status-communities/src/chat.ts +++ b/packages/status-communities/src/chat.ts @@ -1,13 +1,23 @@ import { ChatMessage } from "./chat_message"; import { chatIdToContentTopic } from "./contentTopic"; +import { createSymKeyFromPassword } from "./encryption"; +/** + * Represent a chat room. Only public chats are currently supported. + */ export class Chat { private lastClockValue?: number; private lastMessage?: ChatMessage; - public id: string; - constructor(id: string) { - this.id = id; + private constructor(public id: string, public symKey: Uint8Array) {} + + /** + * Create a public chat room. + */ + public static async create(id: string) { + const symKey = await createSymKeyFromPassword(id); + + return new Chat(id, symKey); } public get contentTopic(): string { diff --git a/packages/status-communities/src/encryption.ts b/packages/status-communities/src/encryption.ts new file mode 100644 index 0000000..4aa297b --- /dev/null +++ b/packages/status-communities/src/encryption.ts @@ -0,0 +1,9 @@ +import { kdf } from "ecies-geth"; + +const AESKeyLength = 32; // bytes + +export async function createSymKeyFromPassword( + password: string +): Promise { + return kdf(Buffer.from(password, "utf-8"), AESKeyLength); +} diff --git a/packages/status-communities/src/messenger.spec.ts b/packages/status-communities/src/messenger.spec.ts index e6441f7..dd074b8 100644 --- a/packages/status-communities/src/messenger.spec.ts +++ b/packages/status-communities/src/messenger.spec.ts @@ -47,8 +47,8 @@ describe("Messenger", () => { it("Sends & Receive message in public chat", async function () { this.timeout(10_000); - messengerAlice.joinChat(testChatId); - messengerBob.joinChat(testChatId); + await messengerAlice.joinChat(testChatId); + await messengerBob.joinChat(testChatId); const text = "This is a message."; diff --git a/packages/status-communities/src/messenger.ts b/packages/status-communities/src/messenger.ts index 0436a96..05cfbb8 100644 --- a/packages/status-communities/src/messenger.ts +++ b/packages/status-communities/src/messenger.ts @@ -39,10 +39,12 @@ export class Messenger { * * Use `addListener` to get messages received on this chat. */ - public joinChat(chatId: string) { + public async joinChat(chatId: string) { if (this.chatsById.has(chatId)) throw "Chat already joined"; - const chat = new Chat(chatId); + const chat = await Chat.create(chatId); + + this.waku.relay.addDecryptionKey(chat.symKey); this.waku.relay.addObserver( (wakuMessage: WakuMessage) => { @@ -72,7 +74,7 @@ export class Messenger { */ public async sendMessage(text: string, chatId: string): Promise { const chat = this.chatsById.get(chatId); - if (!chat) throw `Chat not joined: ${chatId}`; + if (!chat) throw `Failed to send message, chat not joined: ${chatId}`; const chatMessage = chat.createMessage(text); @@ -82,10 +84,11 @@ export class Messenger { this.identity ); - // TODO: Use version 1 with signature + // TODO: Add signature const wakuMessage = await WakuMessage.fromBytes( appMetadataMessage.encode(), - chat.contentTopic + chat.contentTopic, + { symKey: chat.symKey } ); await this.waku.relay.send(wakuMessage); diff --git a/yarn.lock b/yarn.lock index c5160f6..7a787ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1734,7 +1734,7 @@ __metadata: languageName: node linkType: hard -"ecies-geth@npm:^1.5.2": +"ecies-geth@npm:^1.5.2, ecies-geth@npm:^1.5.3": version: 1.5.3 resolution: "ecies-geth@npm:1.5.3" dependencies: @@ -5635,6 +5635,7 @@ fsevents@~2.3.2: "@typescript-eslint/parser": ^4.31.1 buffer: ^6.0.3 chai: ^4.3.4 + ecies-geth: ^1.5.3 eslint: ^7.32.0 eslint-config-prettier: ^8.3.0 eslint-import-resolver-node: ^0.3.6 From 14849fe62f5eb6636c218159a930060c3554cd13 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 23 Sep 2021 15:47:21 +1000 Subject: [PATCH 2/3] Add signature to Waku messages --- packages/status-communities/src/identity.ts | 2 +- packages/status-communities/src/messenger.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/status-communities/src/identity.ts b/packages/status-communities/src/identity.ts index fd66568..2d4c4ae 100644 --- a/packages/status-communities/src/identity.ts +++ b/packages/status-communities/src/identity.ts @@ -6,7 +6,7 @@ import { utils } from "js-waku"; import * as secp256k1 from "secp256k1"; export class Identity { - public constructor(private privateKey: Uint8Array) {} + public constructor(public privateKey: Uint8Array) {} public static generate(): Identity { const privateKey = generatePrivateKey(); diff --git a/packages/status-communities/src/messenger.ts b/packages/status-communities/src/messenger.ts index 05cfbb8..6e65d2c 100644 --- a/packages/status-communities/src/messenger.ts +++ b/packages/status-communities/src/messenger.ts @@ -84,11 +84,10 @@ export class Messenger { this.identity ); - // TODO: Add signature const wakuMessage = await WakuMessage.fromBytes( appMetadataMessage.encode(), chat.contentTopic, - { symKey: chat.symKey } + { symKey: chat.symKey, sigPrivKey: this.identity.privateKey } ); await this.waku.relay.send(wakuMessage); From 30e123a418a7cdd0faf96a724a39e29537517be5 Mon Sep 17 00:00:00 2001 From: Franck Royer Date: Thu, 23 Sep 2021 16:08:06 +1000 Subject: [PATCH 3/3] Add API to retrieve signers on a message --- .../src/application_metadata_message.ts | 13 ++++++++ packages/status-communities/src/identity.ts | 7 ++++ .../status-communities/src/messenger.spec.ts | 33 +++++++++++++++++-- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/packages/status-communities/src/application_metadata_message.ts b/packages/status-communities/src/application_metadata_message.ts index 63d5658..bfd5c95 100644 --- a/packages/status-communities/src/application_metadata_message.ts +++ b/packages/status-communities/src/application_metadata_message.ts @@ -1,4 +1,7 @@ +import { keccak256 } from "js-sha3"; +import { utils } from "js-waku"; import { Reader } from "protobufjs"; +import secp256k1 from "secp256k1"; import { ChatMessage } from "./chat_message"; import { Identity } from "./identity"; @@ -58,4 +61,14 @@ export class ApplicationMetadataMessage { return ChatMessage.decode(this.payload); } + + public get signer(): Uint8Array | undefined { + if (!this.signature || !this.payload) return; + + const signature = this.signature.slice(0, 64); + const recid = this.signature.slice(64)[0]; + const hash = keccak256(this.payload); + + return secp256k1.ecdsaRecover(signature, recid, utils.hexToBuf(hash)); + } } diff --git a/packages/status-communities/src/identity.ts b/packages/status-communities/src/identity.ts index 2d4c4ae..88ec6e8 100644 --- a/packages/status-communities/src/identity.ts +++ b/packages/status-communities/src/identity.ts @@ -26,4 +26,11 @@ export class Identity { return Buffer.concat([signature, Buffer.from([recid])]); } + + /** + * Returns the compressed public key. + */ + public get publicKey(): Uint8Array { + return secp256k1.publicKeyCreate(this.privateKey, true); + } } diff --git a/packages/status-communities/src/messenger.spec.ts b/packages/status-communities/src/messenger.spec.ts index dd074b8..fa93e2c 100644 --- a/packages/status-communities/src/messenger.spec.ts +++ b/packages/status-communities/src/messenger.spec.ts @@ -1,4 +1,5 @@ import { expect } from "chai"; +import { utils } from "js-waku"; import { ApplicationMetadataMessage } from "./application_metadata_message"; import { Identity } from "./identity"; @@ -9,12 +10,14 @@ const testChatId = "test-chat-id"; describe("Messenger", () => { let messengerAlice: Messenger; let messengerBob: Messenger; + let identityAlice: Identity; + let identityBob: Identity; beforeEach(async function () { this.timeout(10_000); - const identityAlice = Identity.generate(); - const identityBob = Identity.generate(); + identityAlice = Identity.generate(); + identityBob = Identity.generate(); [messengerAlice, messengerBob] = await Promise.all([ Messenger.create(identityAlice), @@ -44,7 +47,7 @@ describe("Messenger", () => { ]); }); - it("Sends & Receive message in public chat", async function () { + it("Sends & Receive public chat messages", async function () { this.timeout(10_000); await messengerAlice.joinChat(testChatId); @@ -66,6 +69,30 @@ describe("Messenger", () => { expect(receivedMessage.chatMessage?.text).to.eq(text); }); + it("public chat messages have signers", async function () { + this.timeout(10_000); + + await messengerAlice.joinChat(testChatId); + await messengerBob.joinChat(testChatId); + + const text = "This is a message."; + + const receivedMessagePromise: Promise = + new Promise((resolve) => { + messengerBob.addObserver((message) => { + resolve(message); + }, testChatId); + }); + + await messengerAlice.sendMessage(text, testChatId); + + const receivedMessage = await receivedMessagePromise; + + expect(utils.bufToHex(receivedMessage.signer!)).to.eq( + utils.bufToHex(identityAlice.publicKey) + ); + }); + afterEach(async function () { this.timeout(5000); await messengerAlice.stop();