From 161cd004a73c9f1b3077f4075b2f363f4e9dbebe Mon Sep 17 00:00:00 2001 From: Zef Hemel Date: Thu, 25 Jul 2024 20:29:13 +0200 Subject: [PATCH] Cleaning e2e encryption stuff --- cmd/server.ts | 21 +- .../spaces/encrypted_space_primitives.test.ts | 62 ---- common/spaces/encrypted_space_primitives.ts | 340 ------------------ lib/crypto.ts | 136 ------- server/http_server.ts | 4 - server/instance.ts | 18 +- web/client.ts | 184 ++++------ web/index.html | 5 +- 8 files changed, 71 insertions(+), 699 deletions(-) delete mode 100644 common/spaces/encrypted_space_primitives.test.ts delete mode 100644 common/spaces/encrypted_space_primitives.ts diff --git a/cmd/server.ts b/cmd/server.ts index 33b6f778..faf191a5 100644 --- a/cmd/server.ts +++ b/cmd/server.ts @@ -23,7 +23,6 @@ export async function serveCommand( key?: string; reindex?: boolean; syncOnly?: boolean; - clientEncryption?: boolean; }, folder?: string, ) { @@ -32,15 +31,6 @@ export async function serveCommand( const port = options.port || (Deno.env.get("SB_PORT") && +Deno.env.get("SB_PORT")!) || 3000; - const clientEncryption = options.clientEncryption || - !!Deno.env.get("SB_CLIENT_ENCRYPTION"); - - if (clientEncryption) { - console.log( - "Running in client encryption mode, this will implicitly enable sync-only mode", - ); - } - const syncOnly = options.syncOnly || !!Deno.env.get("SB_SYNC_ONLY"); const readOnly = !!Deno.env.get("SB_READ_ONLY"); @@ -92,7 +82,6 @@ export async function serveCommand( syncOnly, readOnly, shellBackend: backendConfig, - clientEncryption, enableSpaceScript, pagesPath: folder, }); @@ -125,14 +114,20 @@ export async function serveCommand( const manifestDescription = Deno.env.get("SB_DESCRIPTION"); if (manifestName || manifestDescription) { - const manifestData = JSON.parse(clientAssets.readTextFileSync('.client/manifest.json')); + const manifestData = JSON.parse( + clientAssets.readTextFileSync(".client/manifest.json"), + ); if (manifestName) { manifestData.name = manifestData.short_name = manifestName; } if (manifestDescription) { manifestData.description = manifestDescription; } - clientAssets.writeTextFileSync('.client/manifest.json', 'application/json', JSON.stringify(manifestData)); + clientAssets.writeTextFileSync( + ".client/manifest.json", + "application/json", + JSON.stringify(manifestData), + ); } const httpServer = new HttpServer({ hostname, diff --git a/common/spaces/encrypted_space_primitives.test.ts b/common/spaces/encrypted_space_primitives.test.ts deleted file mode 100644 index b347a701..00000000 --- a/common/spaces/encrypted_space_primitives.test.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { MemoryKvPrimitives } from "$lib/data/memory_kv_primitives.ts"; -import { assert, assertEquals } from "$std/testing/asserts.ts"; -import { ChunkedKvStoreSpacePrimitives } from "./chunked_datastore_space_primitives.ts"; -import { EncryptedSpacePrimitives } from "./encrypted_space_primitives.ts"; -import { testSpacePrimitives } from "./space_primitives.test.ts"; - -Deno.test("Encrypted Space Primitives", async () => { - // Using an in-memory store for testing - const memoryKv = new MemoryKvPrimitives(); - const spacePrimitives = new EncryptedSpacePrimitives( - new ChunkedKvStoreSpacePrimitives( - memoryKv, - 1024 * 1024, - ), - ); - assertEquals(false, await spacePrimitives.init()); - await spacePrimitives.setup("password"); - assertEquals(await spacePrimitives.fetchFileList(), []); - await testSpacePrimitives(spacePrimitives); - - // Let's try an incorrect password - try { - await spacePrimitives.login("wronk"); - assert(false); - } catch (e: any) { - assertEquals(e.message, "Incorrect password"); - } - - // Now let's update the password - await spacePrimitives.updatePassword("password", "password2"); - - try { - await spacePrimitives.updatePassword("password", "password2"); - assert(false); - } catch (e: any) { - assertEquals(e.message, "Incorrect password"); - } - - await spacePrimitives.writeFile( - "test.txt", - new TextEncoder().encode("Hello World"), - ); - - // Let's do this again with the new password - - const spacePrimitives2 = new EncryptedSpacePrimitives( - new ChunkedKvStoreSpacePrimitives( - memoryKv, - 1024 * 1024, - ), - ); - assertEquals(true, await spacePrimitives2.init()); - await spacePrimitives2.login("password2"); - assertEquals( - new TextDecoder().decode( - (await spacePrimitives2.readFile("test.txt")).data, - ), - "Hello World", - ); - await spacePrimitives2.deleteFile("test.txt"); - await testSpacePrimitives(spacePrimitives2); -}); diff --git a/common/spaces/encrypted_space_primitives.ts b/common/spaces/encrypted_space_primitives.ts deleted file mode 100644 index cbfd6d35..00000000 --- a/common/spaces/encrypted_space_primitives.ts +++ /dev/null @@ -1,340 +0,0 @@ -import { FileMeta } from "../../plug-api/types.ts"; -import { - base32Decode, - base32Encode, - decryptAES, - deriveKeyFromPassword, - encryptAES, - exportKey, - generateSalt, - importKey, -} from "../../lib/crypto.ts"; -import { plugPrefix } from "./constants.ts"; -import { SpacePrimitives } from "./space_primitives.ts"; - -export const encryptedFileExt = ".crypt"; -export const keyPath = "KEY"; -export const saltFile = "salt.crypt"; - -/** - * This class adds an (AES) based encryption layer on top of another SpacePrimitives implementation. - * It encrypts all file names and file contents. - * It uses a key file (default named _KEY) to store the encryption key, this file is encrypted with a key derived from the user's password. - * The reason to keep the actualy encryption key in a file is to allow the user to change their password without having to re-encrypt all files. - * Important note: FileMeta's size will reflect the underlying encrypted size, not the original size - */ -export class EncryptedSpacePrimitives implements SpacePrimitives { - private masterKey?: CryptoKey; - private encryptedKeyFileName?: string; - spaceSalt?: Uint8Array; - - constructor( - private wrapped: SpacePrimitives, - ) { - } - - /** - * Checks if the space is initialized by loading the salt file. - * @returns true if the space was initialized, false if it was not initialized yet - */ - async init(salt?: Uint8Array | undefined | null): Promise { - if (salt) { - this.spaceSalt = salt; - return true; - } - try { - this.spaceSalt = (await this.wrapped.readFile(saltFile)).data; - return true; - } catch (e: any) { - if (e.message === "Not found") { - console.warn("Space not initialized"); - return false; - } - throw e; - } - } - - /** - * Setup a fresh space with a new salt and master encryption key derived from a password - * @param password - */ - async setup(password: string): Promise { - if (this.spaceSalt) { - throw new Error("Space already initialized"); - } - this.spaceSalt = generateSalt(); - await this.wrapped.writeFile(saltFile, this.spaceSalt); - await this.createKey(password); - } - - /** - * Loads the encryption key from the master key based on the user's password - * @param password the user's password - */ - async login(password: string): Promise { - if (!this.spaceSalt) { - throw new Error("Space not initialized"); - } - // First derive an encryption key solely used for encrypting the key file from the user's password - const keyEncryptionKey = await deriveKeyFromPassword( - this.spaceSalt!, - password, - ); - const encryptedKeyFileName = await this.encryptPath( - keyEncryptionKey, - keyPath, - ); - - try { - this.masterKey = await importKey( - await decryptAES( - keyEncryptionKey, - (await this.wrapped.readFile( - encryptedKeyFileName, - )).data, - ), - ); - this.encryptedKeyFileName = encryptedKeyFileName; - } catch (e: any) { - if (e.message === "Not found") { - throw new Error("Incorrect password"); - } - console.trace(); - throw e; - } - } - - private generateKey(): Promise { - return window.crypto.subtle.generateKey( - { - name: "AES-GCM", - length: 256, - }, - true, - ["encrypt", "decrypt"], - ); - } - - private async createKey(password: string): Promise { - const keyEncryptionKey = await deriveKeyFromPassword( - this.spaceSalt!, - password, - ); - this.encryptedKeyFileName = await this.encryptPath( - keyEncryptionKey, - keyPath, - ); - this.masterKey = await this.generateKey(); - // And write it - await this.wrapped.writeFile( - this.encryptedKeyFileName, - await encryptAES( - keyEncryptionKey, - await exportKey(this.masterKey), - ), - ); - } - - async updatePassword(oldPassword: string, newPasword: string): Promise { - if (!this.masterKey) { - throw new Error("No key loaded"); - } - const oldPasswordKeyFileName = await this.encryptPath( - await deriveKeyFromPassword(this.spaceSalt!, oldPassword), - keyPath, - ); - - // Check if the old password is correct - try { - await this.wrapped.getFileMeta(oldPasswordKeyFileName); - } catch (e: any) { - if (e.message === "Not found") { - throw new Error("Incorrect password"); - } else { - throw e; - } - } - - // First derive an encryption key solely used for encrypting the key file from the user's password - const keyEncryptionKey = await deriveKeyFromPassword( - this.spaceSalt!, - newPasword, - ); - - this.encryptedKeyFileName = await this.encryptPath( - keyEncryptionKey, - keyPath, - ); - // And write it - await this.wrapped.writeFile( - this.encryptedKeyFileName, - await encryptAES( - keyEncryptionKey, - await exportKey(this.masterKey), - ), - ); - - // Then delete the old key file based on the old password - await this.wrapped.deleteFile(oldPasswordKeyFileName); - } - - isUnencryptedPath(name: string) { - return name.startsWith(plugPrefix); - } - - /** - * Left pads a string with zeros to a length of 32, encrypts it using AES-GCM and returns the base32 encoded ciphertext - * @param key - * @param path - * @returns - */ - async encryptPath( - key: CryptoKey, - path: string, - ): Promise { - if (!this.spaceSalt) { - throw new Error("Space not initialized"); - } - if (this.isUnencryptedPath(path)) { - return path; - } - - path = path.padEnd(32, "\0"); - const encodedMessage = new TextEncoder().encode(path); - - const ciphertext = await crypto.subtle.encrypt( - { - name: "AES-GCM", - iv: this.spaceSalt, - }, - key, - encodedMessage, - ); - const encodedPath = base32Encode(new Uint8Array(ciphertext)); - // console.log(new TextDecoder().decode(ciphertext)); - return encodedPath.slice(0, 3) + "/" + encodedPath.slice(3) + - encryptedFileExt; - } - - private async decryptPath( - key: CryptoKey, - encryptedPath: string, - ): Promise { - if (!this.spaceSalt) { - throw new Error("Space not initialized"); - } - if (this.isUnencryptedPath(encryptedPath)) { - return encryptedPath; - } - - if (!encryptedPath.endsWith(encryptedFileExt)) { - throw new Error("Invalid encrypted path"); - } - // Remove the extension and slashes - encryptedPath = encryptedPath.slice(0, -encryptedFileExt.length).replaceAll( - "/", - "", - ); - - // console.log("To decrypt", encryptedPath); - const decrypted = await crypto.subtle.decrypt( - { - name: "AES-GCM", - iv: this.spaceSalt, - }, - key, - base32Decode(encryptedPath), - ); - // Decode the buffer and remove the padding - return removePadding(new TextDecoder().decode(decrypted), "\0"); - } - - async fetchFileList(): Promise { - const files = await this.wrapped.fetchFileList(); - // console.log(files); - return Promise.all( - files.filter((fileMeta) => - fileMeta.name !== this.encryptedKeyFileName && - fileMeta.name !== saltFile - ) - .map(async (fileMeta) => { - return { - ...fileMeta, - name: await this.decryptPath(this.masterKey!, fileMeta.name), - }; - }), - ); - } - - async getFileMeta(name: string): Promise { - if (this.isUnencryptedPath(name)) { - return this.wrapped.getFileMeta(name); - } - const fileMeta = await this.wrapped.getFileMeta( - await this.encryptPath(this.masterKey!, name), - ); - return { - ...fileMeta, - name, - }; - } - - async readFile(name: string): Promise<{ data: Uint8Array; meta: FileMeta }> { - if (this.isUnencryptedPath(name)) { - return this.wrapped.readFile(name); - } - const { data, meta } = await this.wrapped.readFile( - await this.encryptPath(this.masterKey!, name), - ); - return { - data: await decryptAES(this.masterKey!, data), - meta: { - ...meta, - name, - }, - }; - } - - async writeFile( - name: string, - data: Uint8Array, - selfUpdate?: boolean | undefined, - meta?: FileMeta | undefined, - ): Promise { - if (this.isUnencryptedPath(name)) { - return this.wrapped.writeFile(name, data, selfUpdate, meta); - } - const newMeta = await this.wrapped.writeFile( - await this.encryptPath(this.masterKey!, name), - await encryptAES(this.masterKey!, data), - selfUpdate, - meta, - ); - return { - ...newMeta, - name, - }; - } - - async deleteFile(name: string): Promise { - if (this.isUnencryptedPath(name)) { - return this.wrapped.deleteFile(name); - } - return this.wrapped.deleteFile( - await this.encryptPath(this.masterKey!, name), - ); - } -} - -function removePadding(str: string, paddingChar: string): string { - // let startIndex = 0; - // while (startIndex < str.length && str[startIndex] === paddingChar) { - // startIndex++; - // } - // return str.substring(startIndex); - let endIndex = str.length - 1; - while (endIndex >= 0 && str[endIndex] === paddingChar) { - endIndex--; - } - return str.substring(0, endIndex + 1); -} diff --git a/lib/crypto.ts b/lib/crypto.ts index 0f2e8d17..eb0b79c4 100644 --- a/lib/crypto.ts +++ b/lib/crypto.ts @@ -11,142 +11,6 @@ export function simpleHash(s: string): number { return hash; } -export function base32Encode(data: Uint8Array): string { - const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - let result = ""; - let bits = 0; - let value = 0; - for (const byte of data) { - value = (value << 8) | byte; - bits += 8; - while (bits >= 5) { - result += alphabet[(value >>> (bits - 5)) & 31]; - bits -= 5; - } - } - if (bits > 0) { - result += alphabet[(value << (5 - bits)) & 31]; - } - return result; -} - -export function base32Decode(data: string): Uint8Array { - const alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; - const result = new Uint8Array(Math.floor(data.length * 5 / 8)); - let bits = 0; - let value = 0; - let index = 0; - for (const char of data) { - value = (value << 5) | alphabet.indexOf(char); - bits += 5; - if (bits >= 8) { - result[index++] = (value >>> (bits - 8)) & 255; - bits -= 8; - } - } - return result; -} - -export async function deriveKeyFromPassword( - salt: Uint8Array, - password: string, -): Promise { - const baseKey = new TextEncoder().encode(password); - const importedKey = await window.crypto.subtle.importKey( - "raw", - baseKey, - { name: "PBKDF2" }, - false, - ["deriveKey"], - ); - return crypto.subtle.deriveKey( - { - name: "PBKDF2", - salt, - iterations: 10000, - hash: "SHA-256", - }, - importedKey, - { - name: "AES-GCM", - length: 256, - }, - true, - ["encrypt", "decrypt"], - ); -} - -/** - * Encrypts using AES-GCM and prepends the IV to the ciphertext - * @param key - * @param message - * @returns - */ -export async function encryptAES( - key: CryptoKey, - message: Uint8Array, -): Promise { - const iv = crypto.getRandomValues(new Uint8Array(12)); - const ciphertext = await window.crypto.subtle.encrypt( - { - name: "AES-GCM", - iv: iv, - }, - key, - message, - ); - return appendBuffer(iv, new Uint8Array(ciphertext)); -} - -/** - * Decrypts using AES-GCM and expects the IV to be prepended to the ciphertext - * @param key - * @param data - * @returns - */ -export async function decryptAES( - key: CryptoKey, - data: Uint8Array, -): Promise { - const iv = data.slice(0, 12); - const ciphertext = data.slice(12); - const decrypted = await window.crypto.subtle.decrypt( - { - name: "AES-GCM", - iv: iv, - }, - key, - ciphertext, - ); - return new Uint8Array(decrypted); -} - -export function generateSalt(): Uint8Array { - return crypto.getRandomValues(new Uint8Array(16)); -} - -export async function exportKey(key: CryptoKey): Promise { - const arrayBuffer = await window.crypto.subtle.exportKey("raw", key); - return new Uint8Array(arrayBuffer); -} - -export function importKey(key: Uint8Array): Promise { - return window.crypto.subtle.importKey( - "raw", - key, - { name: "AES-GCM" }, - true, - ["encrypt", "decrypt"], - ); -} - -function appendBuffer(buffer1: Uint8Array, buffer2: Uint8Array): Uint8Array { - const tmp = new Uint8Array(buffer1.byteLength + buffer2.byteLength); - tmp.set(new Uint8Array(buffer1), 0); - tmp.set(new Uint8Array(buffer2), buffer1.byteLength); - return tmp; -} - export function base64Decode(s: string): Uint8Array { const binString = atob(s); const len = binString.length; diff --git a/server/http_server.ts b/server/http_server.ts index 0db1ee68..2b804587 100644 --- a/server/http_server.ts +++ b/server/http_server.ts @@ -161,9 +161,6 @@ export class HttpServer { ).replace( "{{CONTENT}}", html, - ).replace( - "{{CLIENT_ENCRYPTION}}", - spaceServer.clientEncryption ? "true" : "false", ); return c.html( html, @@ -262,7 +259,6 @@ export class HttpServer { "{{CONFIG_HASH}}", base64Encode( JSON.stringify([ - spaceServer.clientEncryption, spaceServer.syncOnly, spaceServer.readOnly, spaceServer.enableSpaceScript, diff --git a/server/instance.ts b/server/instance.ts index 94fa4a37..ba5f6436 100644 --- a/server/instance.ts +++ b/server/instance.ts @@ -30,7 +30,6 @@ export type SpaceServerConfig = { syncOnly: boolean; readOnly: boolean; enableSpaceScript: boolean; - clientEncryption: boolean; }; // Equivalent of Client on the server @@ -48,7 +47,6 @@ export class SpaceServer { // Only set when syncOnly == false private serverSystem?: ServerSystem; system?: System; - clientEncryption: boolean; syncOnly: boolean; readOnly: boolean; shellBackend: ShellBackend; @@ -63,16 +61,10 @@ export class SpaceServer { this.hostname = config.hostname; this.auth = config.auth; this.authToken = config.authToken; - this.clientEncryption = !!config.clientEncryption; this.syncOnly = config.syncOnly; this.readOnly = config.readOnly; this.enableSpaceScript = config.enableSpaceScript; - if (this.clientEncryption) { - // Sync only will forced on when encryption is enabled - this.syncOnly = true; - } - this.jwtIssuer = new JWTIssuer(kvPrimitives); this.shellBackend = config.readOnly @@ -143,14 +135,6 @@ export class SpaceServer { } async reloadSettings() { - if (!this.clientEncryption) { - // Only attempt this when the space is not encrypted - this.settings = await ensureAndLoadSettingsAndIndex(this.spacePrimitives); - } else { - this.settings = { - indexPage: "index", - actionButtons: [], - }; - } + this.settings = await ensureAndLoadSettingsAndIndex(this.spacePrimitives); } } diff --git a/web/client.ts b/web/client.ts index c2c4d545..ad9c012e 100644 --- a/web/client.ts +++ b/web/client.ts @@ -47,9 +47,6 @@ import { DataStore } from "$lib/data/datastore.ts"; import { IndexedDBKvPrimitives } from "$lib/data/indexeddb_kv_primitives.ts"; import { DataStoreMQ } from "$lib/data/mq.datastore.ts"; import { DataStoreSpacePrimitives } from "$common/spaces/datastore_space_primitives.ts"; -import { - EncryptedSpacePrimitives, -} from "$common/spaces/encrypted_space_primitives.ts"; import { ensureSpaceIndex } from "$common/space_index.ts"; import { renderTheTemplate } from "$common/syscalls/template.ts"; @@ -75,17 +72,36 @@ declare global { spaceFolderPath: string; syncOnly: boolean; readOnly: boolean; - clientEncryption: boolean; enableSpaceScript: boolean; }; client: Client; } } -// history.scrollRestoration = "manual"; +type WidgetCacheItem = { + height: number; + html: string; + buttons?: CodeWidgetButton[]; + banner?: string; +}; export class Client { + // Event bus used to communicate between components + eventHook = new EventHook(); + + space!: Space; + settings!: BuiltinSettings; + clientSystem!: ClientSystem; + plugSpaceRemotePrimitives!: PlugSpacePrimitives; + httpSpacePrimitives!: HttpSpacePrimitives; + + ui!: MainUI; + stateDataStore!: DataStore; + spaceKV?: KvPrimitives; + mq!: DataStoreMQ; + + // CodeMirror editor editorView!: EditorView; keyHandlerCompartment?: Compartment; @@ -93,10 +109,6 @@ export class Client { private dbPrefix: string; - plugSpaceRemotePrimitives!: PlugSpacePrimitives; - httpSpacePrimitives!: HttpSpacePrimitives; - space!: Space; - saveTimeout?: number; debouncedUpdateEvent = throttle(() => { this.eventHook @@ -104,21 +116,12 @@ export class Client { .catch((e) => console.error("Error dispatching editor:updated event", e)); }, 1000); + // Sync related stuff // Track if plugs have been updated since sync cycle fullSyncCompleted = false; - syncService!: ISyncService; - settings!: BuiltinSettings; - // Event bus used to communicate between components - eventHook!: EventHook; - - ui!: MainUI; - stateDataStore!: DataStore; - spaceKV?: KvPrimitives; - mq!: DataStoreMQ; - - onLoadPageRef: PageRef; + private onLoadPageRef: PageRef; constructor( private parent: Element, @@ -138,19 +141,16 @@ export class Client { * This is a separated from the constructor to allow for async initialization */ async init() { + // Setup the state data store const stateKvPrimitives = new IndexedDBKvPrimitives( `${this.dbPrefix}_state`, ); await stateKvPrimitives.init(); - this.stateDataStore = new DataStore(stateKvPrimitives); // Setup message queue this.mq = new DataStoreMQ(this.stateDataStore); - // Event hook - this.eventHook = new EventHook(); - // Instantiate a PlugOS system this.clientSystem = new ClientSystem( this, @@ -168,11 +168,9 @@ export class Client { this.plugSpaceRemotePrimitives, this.stateDataStore, this.eventHook, - (path) => { - // TODO: At some point we should remove the data.db exception here - return path !== "data.db" && - // Exclude all plug space primitives paths - !this.plugSpaceRemotePrimitives.isLikelyHandled(path) || + (path) => { // isSyncCandidate + // Exclude all plug space primitives paths + return !this.plugSpaceRemotePrimitives.isLikelyHandled(path) || // Except federated ones path.startsWith("!"); }, @@ -194,7 +192,8 @@ export class Client { await this.loadSettings(); await this.loadCaches(); - // Pinging a remote space to ensure we're authenticated properly, if not will result in a redirect to auth page + + // Let's ping the remote space to ensure we're authenticated properly, if not will result in a redirect to auth page try { await this.httpSpacePrimitives.ping(); } catch (e: any) { @@ -225,10 +224,12 @@ export class Client { await this.initNavigator(); await this.initSync(); + // We can load custom styles async this.loadCustomStyles().catch(console.error); await this.dispatchAppEvent("editor:init"); + // Regularly sync the currently open file setInterval(() => { try { this.syncService.syncFile(`${this.currentPage}.md`).catch((e: any) => { @@ -239,6 +240,7 @@ export class Client { } }, pageSyncInterval); + // Let's update the local page list cache asynchronously this.updatePageListCache().catch(console.error); } @@ -262,7 +264,6 @@ export class Client { let initialSync = !await this.syncService.hasInitialSyncCompleted(); this.eventHook.addLocalListener("sync:success", async (operations) => { - // console.log("Operations", operations); if (operations > 0) { // Update the page list await this.space.updatePageList(); @@ -280,7 +281,7 @@ export class Client { console.error, ); } else { // initialSync - // Let's load space scripts, which probably weren't loaded before + // Let's load space scripts again, which probably weren't loaded before await this.clientSystem.loadSpaceScripts(); console.log( "Initial sync completed, now need to do a full space index to ensure all pages are indexed using any custom space script indexers", @@ -298,20 +299,24 @@ export class Client { this.ui.viewDispatch({ type: "sync-change", syncSuccess: true }); }); + this.eventHook.addLocalListener("sync:error", (_name) => { this.ui.viewDispatch({ type: "sync-change", syncSuccess: false }); }); + this.eventHook.addLocalListener("sync:conflict", (name) => { this.flashNotification( `Sync: conflict detected for ${name} - conflict copy created`, "error", ); }); + this.eventHook.addLocalListener("sync:progress", (status: SyncStatus) => { this.showProgress( Math.round(status.filesProcessed / status.totalFiles * 100), ); }); + this.eventHook.addLocalListener( "file:synced", (meta: FileMeta, direction: string) => { @@ -399,7 +404,6 @@ export class Client { adjustedPosition = true; } if (pos !== undefined) { - // setTimeout(() => { this.editorView.dispatch({ selection: { anchor: pos! }, effects: EditorView.scrollIntoView(pos!, { @@ -408,7 +412,6 @@ export class Client { }), }); adjustedPosition = true; - // }); } // If not: just put the cursor at the top of the page, right after the frontmatter @@ -446,6 +449,7 @@ export class Client { // Setup scroll position, cursor position, etc this.navigateWithinPage(pageState); + // Persist this page as the last opened page, we'll use this for cold start PWA loads await this.stateDataStore.set( ["client", "lastOpenedPage"], pageState.page, @@ -453,17 +457,15 @@ export class Client { }); if (location.hash === "#boot" && this.settings.pwaOpenLastPage !== false) { - (async () => { - // Cold start PWA load - const lastPage = await this.stateDataStore.get([ - "client", - "lastOpenedPage", - ]); - if (lastPage) { - console.log("Navigating to last opened page", lastPage); - await this.navigate({ page: lastPage }); - } - })().catch(console.error); + // Cold start PWA load + const lastPage = await this.stateDataStore.get([ + "client", + "lastOpenedPage", + ]); + if (lastPage) { + console.log("Navigating to last opened page", lastPage); + await this.navigate({ page: lastPage }); + } } } @@ -475,74 +477,6 @@ export class Client { let remoteSpacePrimitives: SpacePrimitives = this.httpSpacePrimitives; - if (window.silverBulletConfig.clientEncryption) { - console.log("Enabling encryption"); - - const encryptedSpacePrimitives = new EncryptedSpacePrimitives( - this.httpSpacePrimitives, - ); - remoteSpacePrimitives = encryptedSpacePrimitives; - let loggedIn = false; - // First figure out if we're online & if the key file exists, if not we need to initialize the space - try { - if (!await encryptedSpacePrimitives.init()) { - console.log( - "Space not initialized, will ask for password to initialize", - ); - alert( - "You appear to be accessing a new space with encryption enabled, you will now be asked to create a password", - ); - const password = prompt("Choose a password"); - if (!password) { - alert("Cannot do anything without a password, reloading"); - location.reload(); - throw new Error("Not initialized"); - } - const password2 = prompt("Confirm password"); - if (password !== password2) { - alert("Passwords don't match, reloading"); - location.reload(); - throw new Error("Not initialized"); - } - await encryptedSpacePrimitives.setup(password); - // this.stateDataStore.set(["encryptionKey"], password); - await this.stateDataStore.set( - ["spaceSalt"], - encryptedSpacePrimitives.spaceSalt, - ); - loggedIn = true; - } - } catch (e: any) { - if (e.message === "Offline") { - console.log( - "Offline, will assume encryption space is initialized, fetching salt from data store", - ); - await encryptedSpacePrimitives.init( - await this.stateDataStore.get(["spaceSalt"]), - ); - } - } - if (!loggedIn) { - // Let's ask for the password - try { - await encryptedSpacePrimitives.login( - prompt("Password")!, - ); - await this.stateDataStore.set( - ["spaceSalt"], - encryptedSpacePrimitives.spaceSalt, - ); - } catch (e: any) { - console.log("Got this error", e); - if (e.message === "Incorrect password") { - alert("Incorrect password"); - location.reload(); - } - throw e; - } - } - } - if (this.readOnlyMode) { remoteSpacePrimitives = new ReadOnlySpacePrimitives( remoteSpacePrimitives, @@ -647,15 +581,14 @@ export class Client { ); // Caching a list of known files for the wiki_link highlighter (that checks if a file exists) + // And keeping it up to date as we go this.eventHook.addLocalListener("file:changed", (fileName: string) => { // Make sure this file is in the list of known pages this.clientSystem.allKnownFiles.add(fileName); }); - this.eventHook.addLocalListener("file:deleted", (fileName: string) => { this.clientSystem.allKnownFiles.delete(fileName); }); - this.eventHook.addLocalListener( "file:listed", (allFiles: FileMeta[]) => { @@ -684,6 +617,7 @@ export class Client { return this.eventHook.dispatchEvent(name, ...args); } + // Save the current page save(immediate = false): Promise { return new Promise((resolve, reject) => { if (this.saveTimeout) { @@ -778,6 +712,7 @@ export class Client { startPageNavigate(mode: "page" | "meta" | "all") { // Then show the page navigator this.ui.viewDispatch({ type: "start-navigate", mode }); + // And update the page list cache asynchronously this.updatePageListCache().catch(console.error); } @@ -812,7 +747,9 @@ export class Client { }); } + // Progress circle handling private progressTimeout?: number; + showProgress(progressPerc: number) { this.ui.viewDispatch({ type: "set-progress", @@ -831,6 +768,7 @@ export class Client { ); } + // Various UI elements filterBox( label: string, options: FilterOption[], @@ -924,6 +862,7 @@ export class Client { const line = editorState.doc.lineAt(selection.from); const linePrefix = line.text.slice(0, selection.from - line.from); + // Build up list of parent nodes, some completions need this const parentNodes: string[] = []; const sTree = syntaxTree(editorState); const currentNode = sTree.resolveInner(selection.from); @@ -940,12 +879,15 @@ export class Client { } while (node); } + // Dispatch the event const results = await this.dispatchAppEvent(eventName, { pageName: this.currentPage, linePrefix, pos: selection.from, parentNodes, } as CompleteEvent); + + // Merge results let currentResult: CompletionResult | null = null; for (const result of results) { if (!result) { @@ -975,7 +917,6 @@ export class Client { currentResult = result; } } - // console.log("Compeltion result", actualResult); return currentResult; } @@ -1001,6 +942,7 @@ export class Client { await this.loadPage(this.currentPage); } + // Focus the editor focus() { const viewState = this.ui.viewState; if ( @@ -1102,6 +1044,7 @@ export class Client { perm: "rw", } as PageMeta, }; + // Create new page based on a template this.clientSystem.system.invokeFunction("template.newPage", [pageName]) .then( () => { @@ -1132,7 +1075,7 @@ export class Client { }); // Fetch (possibly) enriched meta data asynchronously - await this.clientSystem.getObjectByRef< + this.clientSystem.getObjectByRef< PageMeta >( this.currentPage, @@ -1298,10 +1241,3 @@ export class Client { return this.widgetCache.get(key); } } - -type WidgetCacheItem = { - height: number; - html: string; - buttons?: CodeWidgetButton[]; - banner?: string; -}; diff --git a/web/index.html b/web/index.html index cea7383d..d29b267f 100644 --- a/web/index.html +++ b/web/index.html @@ -45,7 +45,6 @@ syncOnly: "{{SYNC_ONLY}}" === "true", enableSpaceScript: "{{ENABLE_SPACE_SCRIPT}}" === "true", readOnly: "{{READ_ONLY}}" === "true", - clientEncryption: "{{CLIENT_ENCRYPTION}}" === "true", }; // But in case these variables aren't replaced by the server, fall back sync only mode if (window.silverBulletConfig.spaceFolderPath.includes("{{")) { @@ -54,7 +53,7 @@ syncOnly: true, readOnly: false, enableSpaceScripts: false, - clientEncryption: false, + s }; } @@ -80,4 +79,4 @@ - + \ No newline at end of file