diff --git a/packages/status-communities/package.json b/packages/status-communities/package.json index cf5a4389..09a82cd4 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/application_metadata_message.ts b/packages/status-communities/src/application_metadata_message.ts index 63d56587..bfd5c954 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/chat.ts b/packages/status-communities/src/chat.ts index 2de593a1..20abeb14 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 00000000..4aa297b2 --- /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/identity.ts b/packages/status-communities/src/identity.ts index fd66568e..88ec6e80 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(); @@ -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 e6441f7f..fa93e2c9 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,11 +47,11 @@ describe("Messenger", () => { ]); }); - it("Sends & Receive message in public chat", async function () { + it("Sends & Receive public chat messages", 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."; @@ -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(); diff --git a/packages/status-communities/src/messenger.ts b/packages/status-communities/src/messenger.ts index 0436a96d..6e65d2c3 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,10 @@ export class Messenger { this.identity ); - // TODO: Use version 1 with signature const wakuMessage = await WakuMessage.fromBytes( appMetadataMessage.encode(), - chat.contentTopic + chat.contentTopic, + { symKey: chat.symKey, sigPrivKey: this.identity.privateKey } ); await this.waku.relay.send(wakuMessage); diff --git a/yarn.lock b/yarn.lock index c5160f68..7a787ad0 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