diff --git a/.yarn/cache/@types-pbkdf2-npm-3.1.0-9fa74ff7fb-d15024b195.zip b/.yarn/cache/@types-pbkdf2-npm-3.1.0-9fa74ff7fb-d15024b195.zip new file mode 100644 index 00000000..7afb6d83 Binary files /dev/null and b/.yarn/cache/@types-pbkdf2-npm-3.1.0-9fa74ff7fb-d15024b195.zip differ diff --git a/packages/status-communities/package.json b/packages/status-communities/package.json index 3f7d396b..eec56752 100644 --- a/packages/status-communities/package.json +++ b/packages/status-communities/package.json @@ -25,6 +25,7 @@ "devDependencies": { "@types/chai": "^4.2.22", "@types/mocha": "^9.0.0", + "@types/pbkdf2": "^3.1.0", "@types/secp256k1": "^4.0.3", "@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/parser": "^4.31.1", @@ -47,6 +48,7 @@ "ecies-geth": "^1.5.3", "js-sha3": "^0.8.0", "js-waku": "^0.13.1", + "pbkdf2": "^3.1.2", "protobufjs": "^6.11.2", "secp256k1": "^4.0.2" } diff --git a/packages/status-communities/proto/communities/v1/chat_identity.proto b/packages/status-communities/proto/communities/v1/chat_identity.proto index c455f278..314fb1b1 100644 --- a/packages/status-communities/proto/communities/v1/chat_identity.proto +++ b/packages/status-communities/proto/communities/v1/chat_identity.proto @@ -22,6 +22,8 @@ message ChatIdentity { string description = 5; string color = 6; + + string emoji = 7; } // ProfileImage represents data associated with a user's profile image diff --git a/packages/status-communities/src/chat.ts b/packages/status-communities/src/chat.ts index 54ee5da2..8727fe32 100644 --- a/packages/status-communities/src/chat.ts +++ b/packages/status-communities/src/chat.ts @@ -1,6 +1,7 @@ import { idToContentTopic } from "./contentTopic"; import { createSymKeyFromPassword } from "./encryption"; import { ChatMessage, Content } from "./wire/chat_message"; +import { CommunityChat } from "./wire/community_chat"; /** * Represent a chat room. Only public chats are currently supported. @@ -9,16 +10,23 @@ export class Chat { private lastClockValue?: number; private lastMessage?: ChatMessage; - private constructor(public id: string, public symKey: Uint8Array) {} + private constructor( + public id: string, + public symKey: Uint8Array, + public communityChat?: CommunityChat + ) {} /** * Create a public chat room. * [[Community.instantiateChat]] MUST be used for chats belonging to a community. */ - public static async create(id: string): Promise { + public static async create( + id: string, + communityChat?: CommunityChat + ): Promise { const symKey = await createSymKeyFromPassword(id); - return new Chat(id, symKey); + return new Chat(id, symKey, communityChat); } public get contentTopic(): string { diff --git a/packages/status-communities/src/community.spec.ts b/packages/status-communities/src/community.spec.ts new file mode 100644 index 00000000..bd3490d5 --- /dev/null +++ b/packages/status-communities/src/community.spec.ts @@ -0,0 +1,42 @@ +import { expect } from "chai"; +import { Waku } from "js-waku"; + +import { Community } from "./community"; +import { CommunityDescription } from "./wire/community_description"; + +describe("Community live data", () => { + before(function () { + if (process.env.CI) { + // Skip live data test in CI + this.skip(); + } + }); + + it("Retrieves community description For DappConnect Test from Waku prod fleet", async function () { + this.timeout(20000); + const waku = await Waku.create({ bootstrap: true }); + + await waku.waitForConnectedPeer(); + + const community = await Community.instantiateCommunity( + "0x0262c65c881f5a9f79343a26faaa02aad3af7c533d9445fb1939ed11b8bf4d2abd", + waku + ); + const desc = community.description as CommunityDescription; + expect(desc).to.not.be.undefined; + + expect(desc.identity?.displayName).to.eq("DappConnect Test"); + + const descChats = Array.from(desc.chats.values()).map( + (chat) => chat?.identity?.displayName + ); + expect(descChats).to.include("foobar"); + expect(descChats).to.include("another-channel!"); + + const chats = Array.from(community.chats.values()).map( + (chat) => chat?.communityChat?.identity?.displayName + ); + expect(chats).to.include("foobar"); + expect(chats).to.include("another-channel!"); + }); +}); diff --git a/packages/status-communities/src/community.ts b/packages/status-communities/src/community.ts index 4ffac4f6..424bd8ac 100644 --- a/packages/status-communities/src/community.ts +++ b/packages/status-communities/src/community.ts @@ -11,12 +11,13 @@ const dbg = debug("communities:community"); export class Community { public publicKey: Uint8Array; private waku: Waku; - + public chats: Map; // Chat id, Chat public description?: CommunityDescription; constructor(publicKey: Uint8Array, waku: Waku) { this.publicKey = publicKey; this.waku = waku; + this.chats = new Map(); } /** @@ -60,6 +61,12 @@ export class Community { } this.description = desc; + + await Promise.all( + Array.from(this.description.chats).map(([chatUuid, communityChat]) => { + return this.instantiateChat(chatUuid, communityChat); + }) + ); } /** @@ -68,28 +75,18 @@ export class Community { * * @throws string If the Community Description is unavailable or the chat is not found; */ - public async instantiateChat(chatName: string): Promise { - if (!this.description) { - await this.refreshCommunityDescription(); - if (!this.description) - throw "Failed to retrieve community description, cannot instantiate chat"; - } + private async instantiateChat( + chatUuid: string, + communityChat: CommunityChat + ): Promise { + if (!this.description) + throw "Failed to retrieve community description, cannot instantiate chat"; - let communityChat: CommunityChat | undefined; - let chatUuid: string | undefined; + const chatId = this.publicKeyStr + chatUuid; + if (this.chats.get(chatId)) return; - this.description.chats.forEach((_chat, _id) => { - if (chatUuid) return; + const chat = await Chat.create(chatId, communityChat); - if (_chat.identity?.displayName === chatName) { - chatUuid = _id; - communityChat = _chat; - } - }); - - if (!communityChat || !chatUuid) - throw `Failed to retrieve community community chat with name ${chatName}`; - - return Chat.create(this.publicKeyStr + chatUuid); + this.chats.set(chatId, chat); } } diff --git a/packages/status-communities/src/encryption.spec.ts b/packages/status-communities/src/encryption.spec.ts new file mode 100644 index 00000000..2c16ed94 --- /dev/null +++ b/packages/status-communities/src/encryption.spec.ts @@ -0,0 +1,14 @@ +import { expect } from "chai"; + +import { createSymKeyFromPassword } from "./encryption"; + +describe("Encryption", () => { + it("Generate symmetric key from password", async function () { + const str = "arbitrary data here"; + const symKey = await createSymKeyFromPassword(str); + + expect(Buffer.from(symKey).toString("hex")).to.eq( + "c49ad65ebf2a7b7253bf400e3d27719362a91b2c9b9f54d50a69117021666c33" + ); + }); +}); diff --git a/packages/status-communities/src/encryption.ts b/packages/status-communities/src/encryption.ts index 4aa297b2..e27d06af 100644 --- a/packages/status-communities/src/encryption.ts +++ b/packages/status-communities/src/encryption.ts @@ -1,9 +1,15 @@ -import { kdf } from "ecies-geth"; +import pbkdf2 from "pbkdf2"; const AESKeyLength = 32; // bytes export async function createSymKeyFromPassword( password: string ): Promise { - return kdf(Buffer.from(password, "utf-8"), AESKeyLength); + return pbkdf2.pbkdf2Sync( + Buffer.from(password, "utf-8"), + "", + 65356, + AESKeyLength, + "sha256" + ); } diff --git a/packages/status-communities/src/proto/communities/v1/chat_identity.ts b/packages/status-communities/src/proto/communities/v1/chat_identity.ts index 7b311934..c75a7900 100644 --- a/packages/status-communities/src/proto/communities/v1/chat_identity.ts +++ b/packages/status-communities/src/proto/communities/v1/chat_identity.ts @@ -22,6 +22,7 @@ export interface ChatIdentity { /** description is the user set description, valid only for organisations */ description: string; color: string; + emoji: string; } export interface ChatIdentity_ImagesEntry { @@ -98,6 +99,7 @@ const baseChatIdentity: object = { displayName: "", description: "", color: "", + emoji: "", }; export const ChatIdentity = { @@ -126,6 +128,9 @@ export const ChatIdentity = { if (message.color !== "") { writer.uint32(50).string(message.color); } + if (message.emoji !== "") { + writer.uint32(58).string(message.emoji); + } return writer; }, @@ -161,6 +166,9 @@ export const ChatIdentity = { case 6: message.color = reader.string(); break; + case 7: + message.emoji = reader.string(); + break; default: reader.skipType(tag & 7); break; @@ -202,6 +210,11 @@ export const ChatIdentity = { } else { message.color = ""; } + if (object.emoji !== undefined && object.emoji !== null) { + message.emoji = String(object.emoji); + } else { + message.emoji = ""; + } return message; }, @@ -220,6 +233,7 @@ export const ChatIdentity = { message.description !== undefined && (obj.description = message.description); message.color !== undefined && (obj.color = message.color); + message.emoji !== undefined && (obj.emoji = message.emoji); return obj; }, @@ -258,6 +272,11 @@ export const ChatIdentity = { } else { message.color = ""; } + if (object.emoji !== undefined && object.emoji !== null) { + message.emoji = object.emoji; + } else { + message.emoji = ""; + } return message; }, }; diff --git a/packages/status-communities/src/wire/chat_identity.ts b/packages/status-communities/src/wire/chat_identity.ts index 90bc9dc7..b43c8ecb 100644 --- a/packages/status-communities/src/wire/chat_identity.ts +++ b/packages/status-communities/src/wire/chat_identity.ts @@ -1,6 +1,7 @@ import { Reader } from "protobufjs"; import * as proto from "../proto/communities/v1/chat_identity"; +import { IdentityImage } from "../proto/communities/v1/chat_identity"; export class ChatIdentity { public constructor(public proto: proto.ChatIdentity) {} @@ -14,4 +15,37 @@ export class ChatIdentity { encode(): Uint8Array { return proto.ChatIdentity.encode(this.proto).finish(); } + + /** Lamport timestamp of the message */ + get clock(): number | undefined { + return this.proto.clock; + } + + /** ens_name is the valid ENS name associated with the chat key */ + get ensName(): string | undefined { + return this.proto.ensName; + } + + /** images is a string indexed mapping of images associated with an identity */ + get images(): { [key: string]: IdentityImage } | undefined { + return this.proto.images; + } + + /** display name is the user set identity, valid only for organisations */ + get displayName(): string | undefined { + return this.proto.displayName; + } + + /** description is the user set description, valid only for organisations */ + get description(): string | undefined { + return this.proto.description; + } + + get color(): string | undefined { + return this.proto.color; + } + + get emoji(): string | undefined { + return this.proto.emoji; + } } diff --git a/packages/status-communities/src/wire/community_chat.ts b/packages/status-communities/src/wire/community_chat.ts index 915dfd5b..474c2b99 100644 --- a/packages/status-communities/src/wire/community_chat.ts +++ b/packages/status-communities/src/wire/community_chat.ts @@ -1,12 +1,13 @@ import { Reader } from "protobufjs"; -import { ChatIdentity } from "../proto/communities/v1/chat_identity"; import * as proto from "../proto/communities/v1/communities"; import { CommunityMember, CommunityPermissions, } from "../proto/communities/v1/communities"; +import { ChatIdentity } from "./chat_identity"; + export class CommunityChat { public constructor(public proto: proto.CommunityChat) {} @@ -41,7 +42,9 @@ export class CommunityChat { } public get identity(): ChatIdentity | undefined { - return this.proto.identity; + if (!this.proto.identity) return; + + return new ChatIdentity(this.proto.identity); } // TODO: Document this diff --git a/packages/status-communities/src/wire/community_description.ts b/packages/status-communities/src/wire/community_description.ts index f86d95f8..b4bef503 100644 --- a/packages/status-communities/src/wire/community_description.ts +++ b/packages/status-communities/src/wire/community_description.ts @@ -3,9 +3,11 @@ import { WakuMessage, WakuStore } from "js-waku"; import { Reader } from "protobufjs"; import { idToContentTopic } from "../contentTopic"; +import { createSymKeyFromPassword } from "../encryption"; import * as proto from "../proto/communities/v1/communities"; import { bufToHex } from "../utils"; +import { ApplicationMetadataMessage } from "./application_metadata_message"; import { ChatIdentity } from "./chat_identity"; import { CommunityChat } from "./community_chat"; @@ -45,8 +47,11 @@ export class CommunityDescription { orderedMessages.forEach((message: WakuMessage) => { if (!message.payload) return; try { + const metadata = ApplicationMetadataMessage.decode(message.payload); + if (!metadata.payload) return; + const _communityDescription = CommunityDescription.decode( - message.payload + metadata.payload ); if (!_communityDescription.identity) return; @@ -61,9 +66,12 @@ export class CommunityDescription { }); }; + const symKey = await createSymKeyFromPassword(hexCommunityPublicKey); + await wakuStore .queryHistory([contentTopic], { callback, + decryptionKeys: [symKey], }) .catch((e) => { dbg( diff --git a/yarn.lock b/yarn.lock index 9e2ff6ec..dc797f0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -938,6 +938,15 @@ __metadata: languageName: node linkType: hard +"@types/pbkdf2@npm:^3.1.0": + version: 3.1.0 + resolution: "@types/pbkdf2@npm:3.1.0" + dependencies: + "@types/node": "*" + checksum: d15024b1957c21cf3b8887329d9bd8dfde754cf13a09d76ae25f1391cfc62bb8b8d7b760773c5dbaa748172fba8b3e0c3dbe962af6ccbd69b76df12a48dfba40 + languageName: node + linkType: hard + "@types/prettier@npm:^1.19.0": version: 1.19.1 resolution: "@types/prettier@npm:1.19.1" @@ -8755,7 +8764,7 @@ fsevents@~2.3.2: languageName: node linkType: hard -"pbkdf2@npm:^3.0.3": +"pbkdf2@npm:^3.0.3, pbkdf2@npm:^3.1.2": version: 3.1.2 resolution: "pbkdf2@npm:3.1.2" dependencies: @@ -10487,6 +10496,7 @@ resolve@^2.0.0-next.3: dependencies: "@types/chai": ^4.2.22 "@types/mocha": ^9.0.0 + "@types/pbkdf2": ^3.1.0 "@types/secp256k1": ^4.0.3 "@typescript-eslint/eslint-plugin": ^4.31.1 "@typescript-eslint/parser": ^4.31.1 @@ -10503,6 +10513,7 @@ resolve@^2.0.0-next.3: js-waku: ^0.13.1 mocha: ^9.1.1 npm-run-all: ^4.1.5 + pbkdf2: ^3.1.2 prettier: ^2.4.0 protobufjs: ^6.11.2 secp256k1: ^4.0.2