From e849cf98efb06547f235c9ea2b26b1f7d425a17c Mon Sep 17 00:00:00 2001 From: BlowaterNostr <127284497+BlowaterNostr@users.noreply.github.com> Date: Wed, 12 Jul 2023 15:14:45 +0800 Subject: [PATCH] Encrypted Custom App Data in IndexedDB (#66) --- UI/app.tsx | 38 ++++++----------------- UI/contact-list.ts | 19 +++++++----- UI/dexie-db.ts | 1 - database.ts | 75 ++++++++++++++++++++++++++++++++++++++-------- nostr.ts | 33 ++++++++++++++++---- 5 files changed, 111 insertions(+), 55 deletions(-) diff --git a/UI/app.tsx b/UI/app.tsx index 6289cda..63a2fe6 100644 --- a/UI/app.tsx +++ b/UI/app.tsx @@ -123,7 +123,6 @@ async function initProfileSyncer( // Sync Custom App Data (async () => { - const chan = new Channel<[NostrEvent, string]>(); let subId = newSubID(); let resp = await pool.newSub( subId, @@ -135,31 +134,12 @@ async function initProfileSyncer( if (resp instanceof Error) { throw resp; } - (async () => { - for await (let { res: nostrMessage, url: relayUrl } of resp) { - if (nostrMessage.type === "EVENT" && nostrMessage.event.content) { - const event = nostrMessage.event; - const decryptedEvent = await decryptNostrEvent( - event, - accountContext, - accountContext.publicKey.hex, - ); - if (decryptedEvent instanceof DecryptionFailure) { - console.error(decryptedEvent); - continue; - } - await chan.put([ - decryptedEvent, - relayUrl, - ]); - } + for await (const { res, url } of resp) { + if (res.type == "EVENT") { + database.addEvent(res.event); } - console.log("closed"); - })(); - return chan; - })().then((customAppDataChan) => { - database.syncEvents((e) => e.kind == NostrKind.CustomAppData, customAppDataChan); - }); + } + })(); /////////////////////////////////// // Add relays to Connection Pool // @@ -274,13 +254,13 @@ export function AppComponent(props: { let socialPostsPanel: VNode | undefined; if (model.navigationModel.activeNav == "Social") { const allUserInfo = getAllUsersInformation(app.database, myAccountCtx); - console.log("AppComponent:getSocialPosts before", Date.now() - t); + // console.log("AppComponent:getSocialPosts before", Date.now() - t); const socialPosts = getSocialPosts(app.database, allUserInfo); - console.log("AppComponent:getSocialPosts after", Date.now() - t, Date.now()); + // console.log("AppComponent:getSocialPosts after", Date.now() - t, Date.now()); let focusedContentGetter = () => { - console.log("AppComponent:getFocusedContent before", Date.now() - t); + // console.log("AppComponent:getFocusedContent before", Date.now() - t); let _ = getFocusedContent(model.social.focusedContent, allUserInfo, socialPosts); - console.log("AppComponent:getFocusedContent", Date.now() - t); + // console.log("AppComponent:getFocusedContent", Date.now() - t); if (_?.type === "MessageThread") { let editor = model.social.replyEditors.get(_.data.root.event.id); if (editor == undefined) { diff --git a/UI/contact-list.ts b/UI/contact-list.ts index 87409b6..e5d6e75 100644 --- a/UI/contact-list.ts +++ b/UI/contact-list.ts @@ -99,7 +99,7 @@ function socialPostsStream(pubkeys: Iterable, pool: ConnectionPool) { export function getAllUsersInformation( database: Database_Contextual_View, - myAccountContext: NostrAccountContext, + ctx: NostrAccountContext, ): Map { const t = Date.now(); const res = new Map(); @@ -140,10 +140,10 @@ export function getAllUsersInformation( case NostrKind.DIRECT_MESSAGE: { let whoAm_I_TalkingTo = ""; - if (event.pubkey == myAccountContext.publicKey.hex) { + if (event.pubkey == ctx.publicKey.hex) { // I am the sender whoAm_I_TalkingTo = getTags(event).p[0]; - } else if (getTags(event).p[0] == myAccountContext.publicKey.hex) { + } else if (getTags(event).p[0] == ctx.publicKey.hex) { // I am the receiver whoAm_I_TalkingTo = event.pubkey; } else { @@ -152,7 +152,7 @@ export function getAllUsersInformation( } const userInfo = res.get(whoAm_I_TalkingTo); if (userInfo) { - if (whoAm_I_TalkingTo == myAccountContext.publicKey.hex) { + if (whoAm_I_TalkingTo == ctx.publicKey.hex) { // talking to myself if (userInfo.newestEventSendByMe) { if (event.created_at > userInfo.newestEventSendByMe?.created_at) { @@ -164,7 +164,7 @@ export function getAllUsersInformation( userInfo.newestEventReceivedByMe = event; } } else { - if (myAccountContext.publicKey.hex == event.pubkey) { + if (ctx.publicKey.hex == event.pubkey) { // I am the sender if (userInfo.newestEventSendByMe) { if (event.created_at > userInfo.newestEventSendByMe.created_at) { @@ -192,12 +192,12 @@ export function getAllUsersInformation( newestEventSendByMe: undefined, profile: undefined, }; - if (whoAm_I_TalkingTo == myAccountContext.publicKey.hex) { + if (whoAm_I_TalkingTo == ctx.publicKey.hex) { // talking to myself newUserInfo.newestEventSendByMe = event; newUserInfo.newestEventReceivedByMe = event; } else { - if (myAccountContext.publicKey.hex == event.pubkey) { + if (ctx.publicKey.hex == event.pubkey) { // I am the sender newUserInfo.newestEventSendByMe = event; } else { @@ -212,7 +212,10 @@ export function getAllUsersInformation( case NostrKind.DELETE: break; case NostrKind.CustomAppData: { - const obj: CustomAppData = JSON.parse(event.content); + if (event.kind == NostrKind.CustomAppData) { + event; + } + const obj: CustomAppData = JSON.parse(event.decryptedContent); if (obj.type == "PinContact" || obj.type == "UnpinContact") { const userInfo = res.get(obj.pubkey); if (userInfo) { diff --git a/UI/dexie-db.ts b/UI/dexie-db.ts index 84a6c8b..0c87d38 100644 --- a/UI/dexie-db.ts +++ b/UI/dexie-db.ts @@ -1,5 +1,4 @@ import * as dexie from "https://unpkg.com/dexie@3.2.3/dist/modern/dexie.mjs"; -import { Database_Contextual_View, Indices } from "../database.ts"; import { NostrEvent } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts"; export class DexieDatabase extends dexie.Dexie { diff --git a/database.ts b/database.ts index 07fa2cb..f980cbf 100644 --- a/database.ts +++ b/database.ts @@ -10,7 +10,7 @@ import { NostrKind, RelayResponse_REQ_Message, } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts"; -import { getTags, Tag } from "./nostr.ts"; +import { Decrypted_Nostr_Event, getTags, PlainText_Nostr_Event, Tag } from "./nostr.ts"; import * as csp from "https://raw.githubusercontent.com/BlowaterNostr/csp/master/csp.ts"; import { DexieDatabase } from "./UI/dexie-db.ts"; @@ -25,17 +25,30 @@ export interface Indices { } export class Database_Contextual_View { - private readonly sourceOfChange = csp.chan(buffer_size); - private readonly caster = csp.multi(this.sourceOfChange); + private readonly sourceOfChange = csp.chan(buffer_size); + private readonly caster = csp.multi(this.sourceOfChange); static async New(database: DexieDatabase, ctx: NostrAccountContext) { - const cache: NostrEvent[] = await database.events.filter((_: any) => true).toArray(); + const events: NostrEvent[] = await database.events.filter((_: any) => true).toArray(); + const cache = new Array(); + for (const event of events) { + const e = await transformEvent(event, ctx); + if (e == undefined) { + continue; + } + if (e instanceof Error) { + console.log("Database:delete", event.id); + database.events.delete(event.id); + continue; + } + cache.push(e); + } return new Database_Contextual_View(database, cache, ctx); } constructor( private readonly database: DexieDatabase, - private readonly cache: NostrEvent[], + private readonly cache: (PlainText_Nostr_Event | Decrypted_Nostr_Event)[], private readonly ctx: NostrAccountContext, ) {} @@ -47,19 +60,23 @@ export class Database_Contextual_View { return this.cache.filter(filter); }; - private readonly addToIndexedDB = async (event: NostrEvent) => { - await this.database.events.put(event); - this.cache.push(event); - }; - async addEvent(event: NostrEvent) { const storedEvent = await this.getEvent({ id: event.id }); if (storedEvent) { // event exist return; } console.log("Database.addEvent", event.id); - await this.addToIndexedDB(event); - await this.sourceOfChange.put(event); + await this.database.events.put(event); + const e = await transformEvent(event, this.ctx); + if (e) { + if (e instanceof Error) { + console.log("Database:delete", event.id); + this.database.events.delete(event.id); + return; + } + this.cache.push(e); + await this.sourceOfChange.put(e); + } } syncEvents( @@ -219,3 +236,37 @@ export function whoIamTalkingTo(event: NostrEvent, myPublicKey: PublicKey) { // I am the receiver return whoIAmTalkingTo; } + +async function transformEvent(event: NostrEvent, ctx: NostrAccountContext) { + if (event.kind == NostrKind.CustomAppData) { + if (event.pubkey == ctx.publicKey.hex) { + // if I am the author + const decrypted = await ctx.decrypt(ctx.publicKey.hex, event.content); + if (decrypted instanceof Error) { + return decrypted; + } + const e: Decrypted_Nostr_Event = { + content: event.content, + created_at: event.created_at, + id: event.id, + kind: event.kind, + pubkey: event.pubkey, + sig: event.sig, + tags: event.tags, + decryptedContent: decrypted, + }; + return e; + } + } else { + const e: PlainText_Nostr_Event = { + content: event.content, + created_at: event.created_at, + id: event.id, + kind: event.kind, + pubkey: event.pubkey, + sig: event.sig, + tags: event.tags, + }; + return e; + } +} diff --git a/nostr.ts b/nostr.ts index 799eaf1..cbad8ee 100644 --- a/nostr.ts +++ b/nostr.ts @@ -1,11 +1,7 @@ /* Extension to common Nostr types */ -import { - PrivateKey, - PublicKey, - publicKeyHexFromNpub, -} from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/key.ts"; +import { PrivateKey, PublicKey } from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/key.ts"; import * as nostr from "https://raw.githubusercontent.com/BlowaterNostr/nostr.ts/main/nostr.ts"; import { groupBy, @@ -325,6 +321,33 @@ export interface Unsigned_CustomAppData_Typed_Event { readonly content: CustomAppData; } +export type Decrypted_Nostr_Event = { + readonly id: nostr.EventID; + readonly sig: string; + readonly pubkey: string; + readonly kind: nostr.NostrKind.CustomAppData; + readonly created_at: number; + readonly tags: Tag[]; + readonly content: string; + readonly decryptedContent: string; +}; + +export type PlainText_Nostr_Event = { + readonly id: nostr.EventID; + readonly sig: string; + readonly pubkey: string; + readonly kind: + | NostrKind.DIRECT_MESSAGE + | NostrKind.CONTACTS + | NostrKind.DELETE + | NostrKind.META_DATA + | NostrKind.TEXT_NOTE + | NostrKind.RECOMMED_SERVER; + readonly created_at: number; + readonly tags: Tag[]; + readonly content: string; +}; + export type CustomAppData = PinContact | UnpinContact | UserLogin; export type PinContact = {