Merge pull request #83 from status-im/community-test

This commit is contained in:
Franck Royer 2021-10-19 15:00:37 +11:00 committed by GitHub
commit e8a754f418
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 176 additions and 30 deletions

Binary file not shown.

View File

@ -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"
}

View File

@ -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

View File

@ -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<Chat> {
public static async create(
id: string,
communityChat?: CommunityChat
): Promise<Chat> {
const symKey = await createSymKeyFromPassword(id);
return new Chat(id, symKey);
return new Chat(id, symKey, communityChat);
}
public get contentTopic(): string {

View File

@ -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!");
});
});

View File

@ -11,12 +11,13 @@ const dbg = debug("communities:community");
export class Community {
public publicKey: Uint8Array;
private waku: Waku;
public chats: Map<string, Chat>; // 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<Chat> {
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<void> {
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);
}
}

View File

@ -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"
);
});
});

View File

@ -1,9 +1,15 @@
import { kdf } from "ecies-geth";
import pbkdf2 from "pbkdf2";
const AESKeyLength = 32; // bytes
export async function createSymKeyFromPassword(
password: string
): Promise<Uint8Array> {
return kdf(Buffer.from(password, "utf-8"), AESKeyLength);
return pbkdf2.pbkdf2Sync(
Buffer.from(password, "utf-8"),
"",
65356,
AESKeyLength,
"sha256"
);
}

View File

@ -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;
},
};

View File

@ -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;
}
}

View File

@ -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

View File

@ -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(

View File

@ -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