Cleaning e2e encryption stuff

This commit is contained in:
Zef Hemel
2024-07-25 20:29:13 +02:00
parent af3df5dbeb
commit 161cd004a7
8 changed files with 71 additions and 699 deletions
+8 -13
View File
@@ -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,
@@ -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);
});
-340
View File
@@ -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<boolean> {
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<void> {
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<void> {
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<CryptoKey> {
return window.crypto.subtle.generateKey(
{
name: "AES-GCM",
length: 256,
},
true,
["encrypt", "decrypt"],
);
}
private async createKey(password: string): Promise<void> {
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<void> {
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<string> {
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<string> {
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<FileMeta[]> {
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<FileMeta> {
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<FileMeta> {
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<void> {
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);
}
-136
View File
@@ -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<CryptoKey> {
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<Uint8Array> {
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<Uint8Array> {
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<Uint8Array> {
const arrayBuffer = await window.crypto.subtle.exportKey("raw", key);
return new Uint8Array(arrayBuffer);
}
export function importKey(key: Uint8Array): Promise<CryptoKey> {
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;
-4
View File
@@ -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,
+1 -17
View File
@@ -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<SilverBulletHooks>;
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);
}
}
+60 -124
View File
@@ -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<void> {
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;
};
+2 -3
View File
@@ -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
};
}
</script>
@@ -80,4 +79,4 @@
</div>
</body>
</html>
</html>