From 3c80ac3125623b800babbe81d5a0535f05c2bfbc Mon Sep 17 00:00:00 2001 From: Fabian Stamm Date: Fri, 10 Jul 2020 18:54:32 +0200 Subject: [PATCH] Fix bug with error when decoding --- src/components/Footer.tsx | 119 ++-- src/notes.ts | 1250 +++++++++++++++++++------------------ 2 files changed, 718 insertions(+), 651 deletions(-) diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index c6a3a95..626ef59 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -4,69 +4,76 @@ import Refresh from "feather-icons/dist/icons/refresh-cw.svg"; import "./footer.scss"; import Notifications from "../notifications"; -export class Footer extends Component<{}, { synced: boolean, syncing: boolean }> { - constructor(props) { - super(props); - this.state = { synced: false, syncing: false }; - this.onSyncedChange = this.onSyncedChange.bind(this); - this.onSyncChange = this.onSyncChange.bind(this); - } +export class Footer extends Component< + {}, + { synced: boolean; syncing: boolean } +> { + constructor(props) { + super(props); + this.state = { synced: false, syncing: false }; + this.onSyncedChange = this.onSyncedChange.bind(this); + this.onSyncChange = this.onSyncChange.bind(this); + } - async componentWillMount() { - Notes.syncedObservable.subscribe(this.onSyncedChange); - Notes.syncObservable.subscribe(this.onSyncChange); - this.setState({ synced: await Notes.isSynced() }) - } + async componentWillMount() { + Notes.syncedObservable.subscribe(this.onSyncedChange); + Notes.syncObservable.subscribe(this.onSyncChange); + this.setState({ synced: await Notes.isSynced() }); + } - componentWillUnmount() { - Notes.syncedObservable.unsubscribe(this.onSyncedChange); - Notes.syncObservable.unsubscribe(this.onSyncChange); - } + componentWillUnmount() { + Notes.syncedObservable.unsubscribe(this.onSyncedChange); + Notes.syncObservable.unsubscribe(this.onSyncChange); + } - onSyncChange(state: boolean) { - console.log("sync", state); - this.setState({ syncing: state }) - } + onSyncChange(state: boolean) { + console.log("sync", state); + this.setState({ syncing: state }); + } - onSyncedChange(state: boolean) { - console.log("synced", state); - this.setState({ synced: state }) - } + onSyncedChange(state: boolean) { + console.log("synced", state); + this.setState({ synced: state }); + } - onSyncClick() { - Notes.sync().then(() => { - Notifications.sendInfo("Finished Synchronisation"); - }) - } + onSyncClick() { + Notes.sync().then(() => { + Notifications.sendInfo("Finished Synchronisation"); + }); + } - render() { - let extrac = undefined; - let color; - let text; - if (this.state.syncing) { - color = "orange"; - text = "syncing"; - extrac = "reloading" - } else { - if (this.state.synced) { - color = "green"; - text = "synced"; - } else { - color = "red"; - text = "not synced"; - } - } - return + ); + } +} diff --git a/src/notes.ts b/src/notes.ts index ef5681b..a1be88b 100644 --- a/src/notes.ts +++ b/src/notes.ts @@ -1,595 +1,655 @@ -import SecureFile, { IFile } from "@hibas123/secure-file-wrapper"; -import { Lock, Observable } from "@hibas123/utils"; -import * as aesjs from "aes-js"; -import { IDBPTransaction } from "idb"; -import { sha256 } from "js-sha256"; -import * as uuidv4 from "uuid/v4"; -import * as config from "../config.json"; -import * as b64 from "./helper/base64"; -import IDB from "./helper/indexeddb"; -import Notifications, { MessageType } from "./notifications"; - - -export class HttpError extends Error { - constructor(public status: number, public statusText: string) { - super(statusText); - console.log(statusText); - } -} - -// const newSymbol = Symbol("Symbol for new Notes") - -export interface Note { - _id: string; - folder: string; - time: Date; -} - -interface DBNote extends Note { - __value: Uint8Array; - preview: Uint8Array; -} - -export interface BaseNote extends Note { - preview: string; -} - -export interface ViewNote extends BaseNote { - __value: string; -} - -import clonedeep = require("lodash.clonedeep"); - -const Encoder = new TextEncoder(); -const Decoder = new TextDecoder(); - -enum OpLogType { - CREATE, - CHANGE, - DELETE -} - -interface OpLog { - /** - * Type of operation - */ - type: OpLogType; - - /** - * The value - */ - values: { - value: Uint8Array, - preview: Uint8Array - }; - - /** - * Date of change - */ - date: Date; -} - -export type VaultList = { name: string, encrypted: boolean, id: string }[]; - -export interface IVault { - name: string; - id: string; - encrypted: boolean; - getAllNotes(): Promise; - searchNote(term: string): Promise - newNote(): ViewNote; - saveNote(note: ViewNote, date?: Date): Promise; - getNote(id: string): Promise; - deleteNote(id: string): Promise; -} - -const awaitTimeout = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); - -class NotesProvider { - private notesObservableServer = new Observable() - public notesObservable = this.notesObservableServer.getPublicApi() - - private syncObservableServer = new Observable() - /** - * Will send false once finished and true on start - */ - public syncObservable = this.syncObservableServer.getPublicApi() - - private database = new IDB("notes", ["notes", "oplog"]); - private noteDB = this.database.getStore("notes"); - private oplogDB = this.database.getStore<{ id: string, logs: OpLog[] }>("oplog"); - - private vaultKeys = new Map(); - - - public apiLock = new Lock(); - public apiLockRls = this.apiLock.getLock() - - private syncLock = new Lock(); - - private _secureFile: SecureFile; - - private generalEncryption: Uint8Array = undefined; - - private syncedObservableServer = new Observable(); - public syncedObservable = this.syncedObservableServer.getPublicApi(); - - public async isSynced() { - return (await this.oplogDB.getAll()).length <= 0 - } - - private _name; - public get name() { - return this._name; - } - - loginRequired() { - return !localStorage.getItem("refreshtoken") || !this.generalEncryption; - } - - login() { - window.location.href = `${config.auth_server}/auth?client_id=${config.client_id}&scope=${config.permission}&redirect_uri=${encodeURIComponent(config.callback_url)}&response_type=code` - } - - async getToken(code: string) { - let req = await fetch(`${config.auth_server}/api/oauth/refresh?grant_type=authorization_code&client_id=${config.client_id}&code=${code}`); - let res = await req.json(); - if (!res.error) { - localStorage.setItem("refreshtoken", res.token); - localStorage.setItem("name", res.profile.name); - this._name = res.profile.name; - let kb = this.passwordToKey(res.profile.enc_key); - localStorage.setItem("enc_key", b64.encode(kb)); - this.generalEncryption = kb - } else { - return "Invalid Code" - } - } - - constructor(public readonly baseurl = "") { - this._secureFile = new SecureFile(config.secure_file_server); - this._secureFile.jwtObservable.subscribe(async (callback) => { - try { - let jwt = await this.getJWT(); - callback(null, jwt); - } catch (err) { - callback(err, null); - } - }) - - let key = localStorage.getItem("enc_key"); - if (key) { - this.generalEncryption = b64.decode(key) - } - this._name = localStorage.getItem("name"); - } - - public async start() { - const next = () => { - setTimeout(() => { - this.sync().then(next); - }, 30000) - } - this.syncedObservableServer.send((await this.oplogDB.getAll()).length <= 0); - let prs = this.apiLockRls.then(lock => lock.release()); - prs.then(() => awaitTimeout(2000)).then(() => this.sync()).then(() => next()); - return prs; - } - - private async getJWT() { - let lock = await this.apiLock.getLock() - try { - console.log("Getting JWT"); - let req = await fetch(config.auth_server + "/api/oauth/jwt?refreshtoken=" + localStorage.getItem("refreshtoken")); - if (req.status !== 200) { - Notifications.sendNotification("offline", MessageType.INFO); - throw new Error("Offline") - } - let res = await req.json(); - if (res.error) { - console.log("Refresh token invalid, forward to login") - localStorage.removeItem("refreshtoken"); - this.login() - throw new Error("Need login!") - } else { - return res.token - } - } finally { - lock.release() - } - } - - async Logout() { - localStorage.removeItem("refreshtoken"); - window.location.reload(); - } - - async sync() { - const lock = await this.syncLock.getLock() - const log = (...message: any[]) => { - console.log("[SYNC]: ", ...message) - } - - this.syncObservableServer.send(true); - log("Start") - try { - log("Fetching"); - let [remotes, locals, oplogs] = await Promise.all([this._secureFile.list(), this.noteDB.getAll(), this.oplogDB.getAll()]); - log("Fetched"); - // Create sync pairs (remote & local) - log("Building pairs"); - let pairs: { local: DBNote, remote: IFile, id: string, oplog: OpLog[] }[] = []; - remotes.map(r => { - let lIdx = locals.findIndex(e => e._id === r._id); - let l: DBNote = undefined; - if (lIdx >= 0) { - l = locals[lIdx]; - locals.splice(lIdx, 1); - } - - let oIdx = oplogs.findIndex(e => e.id === r._id); - let oplog: OpLog[]; - if (oIdx >= 0) { - oplog = oplogs[oIdx].logs; - oplogs.splice(oIdx, 1); - } - - pairs.push({ - remote: r, - local: l, - oplog, - id: r._id - }) - }) - - locals.forEach(l => { - let oIdx = oplogs.findIndex(e => e.id === l._id); - let oplog: OpLog[] = undefined; - if (oIdx >= 0) { - oplog = oplogs[oIdx].logs; - if (oplog.length <= 0) oplog = undefined; - oplogs.splice(oIdx, 1); - } - - pairs.push({ - remote: undefined, - local: l, - oplog, - id: l._id - }) - }) - - oplogs.forEach(oplog => { - if (!oplog) return; - if (oplog.logs.length > 0) - pairs.push({ - remote: undefined, - local: undefined, - oplog: oplog.logs, - id: oplog.id - }) - }) - - log("Pairs builded"); - - let stats = { - remote_change: 0, - remote_delete: 0, - remote_create: 0, - local_change: 0, - local_delete: 0, - do_nothing: 0, - error: 0 - } - - log("Start inspection"); - for (let { local, remote, oplog, id } of pairs) { - const apply = async (old = false) => { - log("Apply OPLOG to", id); - for (let op of oplog) { - switch (op.type) { - case OpLogType.CHANGE: - log(id, "REMOTE CHANGE"); - stats.remote_change++; - await this._secureFile.update(id, op.values.value, b64.encode(op.values.preview), op.date, old); - break; - case OpLogType.DELETE: - log(id, "REMOTE DELETE"); - stats.remote_delete++; - if (old) break; // if the deletion is old, just ignore - await this._secureFile.delete(id) - break; - case OpLogType.CREATE: - log(id, "REMOTE CREATE"); - stats.remote_create++; - await this._secureFile.create( - "", - op.values.value, - "binary", - local.folder, - b64.encode(op.values.preview), - id, - op.date - ); - break; - } - } - } - - const localChange = (id: string) => { - //TODO implement - } - - const create = async () => { - log(id, "LOCAL CREATAE/UPDATE"); - stats.local_change++; - let value = await this._secureFile.get(id); - let note: DBNote = { - _id: remote._id, - folder: remote.folder, - preview: b64.decode(remote.active.preview), - time: remote.active.time, - __value: new Uint8Array(value) - } - await this.noteDB.set(id, note); - localChange(id); - } - - try { - // log(id, "LRO: ", !!local, !!remote, !!oplog) - if (remote && !oplog) { - if (local) { - let old = remote.active.time.valueOf() > local.time.valueOf(); - if (old) - await create() - else { - stats.do_nothing++; - log(id, "DO NOTHING"); - } - } else { - await create() - } - } else if (!remote && local && !oplog) { // No local changes, but remote deleted - log("LOCAL DELETE"); - stats.local_delete++; - await this.noteDB.delete(id); - localChange(id); - } else if (!remote && oplog) { // Remote does not exist, but oplog, just apply all changes including possible delete - await apply() - } else if (remote && oplog) { - let last = oplog[oplog.length - 1] - let old = remote.active.time.valueOf() > last.date.valueOf(); - - if (old) - await create() // Will recreate local entry - await apply(old) // Will apply changes to remote - } else { - log(id, "DO NOTHING"); - stats.do_nothing++; - } - } catch (err) { - console.error(err); - stats.error++; - Notifications.sendNotification("Error syncing: " + id, MessageType.ERROR); - } - await this.oplogDB.delete(id); - } - log("Stats", stats); - this.syncedObservableServer.send((await this.oplogDB.getAll()).length <= 0) - } finally { - log("Finished") - lock.release() - this.syncObservableServer.send(false); - } - } - - public forgetVaultKey(vault_id: string) { - this.vaultKeys.delete(vault_id); - localStorage.removeItem("vault_" + vault_id); - } - - public getVaultKey(vault_id: string) { - let key = this.vaultKeys.get(vault_id); - if (!key) { - let lsk = localStorage.getItem("vault_" + vault_id); - if (lsk) { - key = b64.decode(lsk); - this.vaultKeys.set(vault_id, key); - } - } - return key; - } - - public saveVaultKey(vault_id: string, key: Uint8Array, permanent?: boolean) { - this.vaultKeys.set(vault_id, key); - if (permanent) { - localStorage.setItem("vault_" + vault_id, b64.encode(key)); - } - } - - public getVaults(): Promise { - return this.noteDB.getAll() - .then(notes => notes - .filter(e => Decoder.decode(e.preview) === "__VAULT__") - .map(e => { - let value = e.__value; - let encrypted = false; - if (this.decrypt(value) !== "__BASELINE__") encrypted = true; - return { name: e.folder, encrypted, id: e._id } - })); - } - - public async createVault(name: string, key?: Uint8Array) { - let vault: DBNote = { - __value: this.encrypt("__BASELINE__", key), - _id: uuidv4(), - folder: name, - preview: Encoder.encode("__VAULT__"), - time: new Date() - } - let tx = this.database.transaction(); - await Promise.all([ - this.addop(vault._id, OpLogType.CREATE, { - value: vault.__value, - preview: vault.preview - }, vault.time, tx), - this.noteDB.set(vault._id, vault) - ]); - } - - public async getVault(vault_id: string, key?: Uint8Array): Promise { - let vault = await this.noteDB.get(vault_id); - if (!vault) throw new Error("Vault not found!"); - if (this.decrypt(vault.__value, key) !== "__BASELINE__") throw new Error("Invalid password!"); - return new NotesProvider.Vault(vault, key) - } - - public async deleteVault(vault_id: string) { - let vault = await this.noteDB.get(vault_id); - if (!vault) throw new Error("Vault not found!"); - let v = new NotesProvider.Vault(vault); - await Promise.all((await v.getAllNotes()).map(note => this.delete(note._id))); - await this.delete(v.id); // This can also delete a vault - } - - public passwordToKey(password: string) { - return new Uint8Array(sha256.arrayBuffer(password + config.client_id)) - } - - private _encrypt(value: Uint8Array, key?: Uint8Array): Uint8Array { - if (!key) return value; - var aesCtr = new aesjs.ModeOfOperation.ctr(key); - var encryptedBytes = aesCtr.encrypt(value); - return new Uint8Array(encryptedBytes); - } - - private encrypt(value: string, key?: Uint8Array): Uint8Array { - let msg = this._encrypt(Encoder.encode(value), key) - return new Uint8Array(this._encrypt(msg, this.generalEncryption)) - } - - private _decrypt(value: ArrayBuffer, key?: Uint8Array): Uint8Array { - if (!key) return new Uint8Array(value); - var aesCtr = new aesjs.ModeOfOperation.ctr(key); - var decryptedBytes = aesCtr.decrypt(value); - return new Uint8Array(decryptedBytes) - } - - private decrypt(value: ArrayBuffer, key?: Uint8Array): string { - let msg = this._decrypt(value, key) - return Decoder.decode(this._decrypt(msg, this.generalEncryption)) - } - - async addop(note_id: string, type: OpLogType, values: { value: Uint8Array, preview: Uint8Array }, date: Date, transaction?: IDBPTransaction) { - let tx = transaction || this.oplogDB.transaction(); - let oplog = await this.oplogDB.get(note_id, tx); - if (!oplog) oplog = { logs: [], id: note_id }; - oplog.logs.push({ - date: date, - type, - values - }) - this.syncedObservableServer.send(false); - await this.oplogDB.set(note_id, oplog, tx); - } - - async delete(id: string) { - let lock = await this.syncLock.getLock(); - let tx = this.database.transaction(this.oplogDB, this.noteDB) - await Promise.all([ - this.addop(id, OpLogType.DELETE, null, new Date(), tx), - this.noteDB.delete(id, tx) - ]) - lock.release(); - } - - static Vault = class implements IVault { - id: string; - name: string; - encrypted: boolean = false; - constructor(private vault: Note, private key?: Uint8Array) { - if (key) this.encrypted = true; - this.id = vault._id; - this.name = vault.folder; - } - - private encrypt(data: string) { - return Notes.encrypt(data, this.key); - } - - private decrypt(data: ArrayBuffer) { - return Notes.decrypt(data, this.key); - } - - async getAllNotes() { - return Notes.noteDB.getAll() - .then(all => all.filter(e => e.folder === this.vault._id) - .sort(this.sort) - .map(e => { - let new_note = clonedeep(e) as BaseNote - delete (new_note).__value - new_note.preview = this.decrypt(e.preview) - return new_note; - })); - } - - private sort(a: DBNote, b: DBNote) { - return new Date(b.time).getTime() - new Date(a.time).getTime(); - } - - async searchNote(term: string) { - let all = await this.getAllNotes(); - return all.filter(e => e.preview.indexOf(term) >= 0) - } - - newNote(): ViewNote { - return { - _id: uuidv4(), - folder: this.vault._id, - time: new Date(), - __value: "", - preview: "" - } - } - - async saveNote(note: ViewNote, date = new Date()) { - let lock = await Notes.syncLock.getLock(); - const tx = Notes.database.transaction(Notes.noteDB, Notes.oplogDB); - let old_note = await Notes.noteDB.get(note._id, tx); - - let new_note = clonedeep(note) as DBNote; - new_note.__value = this.encrypt(note.__value) - let [title, preview] = note.__value.split("\n"); - if (preview) preview = "\n" + preview; - else preview = "" - new_note.preview = this.encrypt((title + preview).substr(0, 128)) - new_note.time = date; - - await Promise.all([ - Notes.addop(note._id, !old_note ? OpLogType.CREATE : OpLogType.CHANGE, { - value: new_note.__value, - preview: new_note.preview - }, date, tx), - Notes.noteDB.set(note._id, new_note, tx) - ]) - lock.release(); - } - - async getNote(id: string): Promise { - let note = await Notes.noteDB.get(id); - if (!note) return undefined; - let new_note = clonedeep(note) as ViewNote; - new_note.__value = this.decrypt(note.__value); - return new_note; - } - - deleteNote(id: string) { - return Notes.delete(id); - } - } -} - -const Notes = new NotesProvider() -export default Notes; - -(window).api = Notes \ No newline at end of file +import SecureFile, { IFile } from "@hibas123/secure-file-wrapper"; +import { Lock, Observable } from "@hibas123/utils"; +import * as aesjs from "aes-js"; +import { IDBPTransaction } from "idb"; +import { sha256 } from "js-sha256"; +import * as uuidv4 from "uuid/v4"; +import * as config from "../config.json"; +import * as b64 from "./helper/base64"; +import IDB from "./helper/indexeddb"; +import Notifications, { MessageType } from "./notifications"; + +export class HttpError extends Error { + constructor(public status: number, public statusText: string) { + super(statusText); + console.log(statusText); + } +} + +// const newSymbol = Symbol("Symbol for new Notes") + +export interface Note { + _id: string; + folder: string; + time: Date; +} + +interface DBNote extends Note { + __value: Uint8Array; + preview: Uint8Array; +} + +export interface BaseNote extends Note { + preview: string; +} + +export interface ViewNote extends BaseNote { + __value: string; +} + +import clonedeep = require("lodash.clonedeep"); + +const Encoder = new TextEncoder(); +const Decoder = new TextDecoder(); + +enum OpLogType { + CREATE, + CHANGE, + DELETE, +} + +interface OpLog { + /** + * Type of operation + */ + type: OpLogType; + + /** + * The value + */ + values: { + value: Uint8Array; + preview: Uint8Array; + }; + + /** + * Date of change + */ + date: Date; +} + +export type VaultList = { name: string; encrypted: boolean; id: string }[]; + +export interface IVault { + name: string; + id: string; + encrypted: boolean; + getAllNotes(): Promise; + searchNote(term: string): Promise; + newNote(): ViewNote; + saveNote(note: ViewNote, date?: Date): Promise; + getNote(id: string): Promise; + deleteNote(id: string): Promise; +} + +const awaitTimeout = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + +class NotesProvider { + private notesObservableServer = new Observable(); + public notesObservable = this.notesObservableServer.getPublicApi(); + + private syncObservableServer = new Observable(); + /** + * Will send false once finished and true on start + */ + public syncObservable = this.syncObservableServer.getPublicApi(); + + private database = new IDB("notes", ["notes", "oplog"]); + private noteDB = this.database.getStore("notes"); + private oplogDB = this.database.getStore<{ id: string; logs: OpLog[] }>( + "oplog" + ); + + private vaultKeys = new Map(); + + public apiLock = new Lock(); + public apiLockRls = this.apiLock.getLock(); + + private syncLock = new Lock(); + + private _secureFile: SecureFile; + + private generalEncryption: Uint8Array = undefined; + + private syncedObservableServer = new Observable(); + public syncedObservable = this.syncedObservableServer.getPublicApi(); + + public async isSynced() { + return (await this.oplogDB.getAll()).length <= 0; + } + + private _name; + public get name() { + return this._name; + } + + loginRequired() { + return !localStorage.getItem("refreshtoken") || !this.generalEncryption; + } + + login() { + window.location.href = `${config.auth_server}/auth?client_id=${ + config.client_id + }&scope=${config.permission}&redirect_uri=${encodeURIComponent( + config.callback_url + )}&response_type=code`; + } + + async getToken(code: string) { + let req = await fetch( + `${config.auth_server}/api/oauth/refresh?grant_type=authorization_code&client_id=${config.client_id}&code=${code}` + ); + let res = await req.json(); + if (!res.error) { + localStorage.setItem("refreshtoken", res.token); + localStorage.setItem("name", res.profile.name); + this._name = res.profile.name; + let kb = this.passwordToKey(res.profile.enc_key); + localStorage.setItem("enc_key", b64.encode(kb)); + this.generalEncryption = kb; + } else { + return "Invalid Code"; + } + } + + constructor(public readonly baseurl = "") { + this._secureFile = new SecureFile(config.secure_file_server); + this._secureFile.jwtObservable.subscribe(async (callback) => { + try { + let jwt = await this.getJWT(); + callback(null, jwt); + } catch (err) { + callback(err, null); + } + }); + + let key = localStorage.getItem("enc_key"); + if (key) { + this.generalEncryption = b64.decode(key); + } + this._name = localStorage.getItem("name"); + } + + public async start() { + const next = () => { + setTimeout(() => { + this.sync().then(next); + }, 30000); + }; + this.syncedObservableServer.send( + (await this.oplogDB.getAll()).length <= 0 + ); + let prs = this.apiLockRls.then((lock) => lock.release()); + prs.then(() => awaitTimeout(2000)) + .then(() => this.sync()) + .then(() => next()); + return prs; + } + + private async getJWT() { + let lock = await this.apiLock.getLock(); + try { + console.log("Getting JWT"); + let req = await fetch( + config.auth_server + + "/api/oauth/jwt?refreshtoken=" + + localStorage.getItem("refreshtoken") + ); + if (req.status !== 200) { + Notifications.sendNotification("offline", MessageType.INFO); + throw new Error("Offline"); + } + let res = await req.json(); + if (res.error) { + console.log("Refresh token invalid, forward to login"); + localStorage.removeItem("refreshtoken"); + this.login(); + throw new Error("Need login!"); + } else { + return res.token; + } + } finally { + lock.release(); + } + } + + async Logout() { + localStorage.removeItem("refreshtoken"); + window.location.reload(); + } + + async sync() { + const lock = await this.syncLock.getLock(); + const log = (...message: any[]) => { + console.log("[SYNC]: ", ...message); + }; + + this.syncObservableServer.send(true); + log("Start"); + try { + log("Fetching"); + let [remotes, locals, oplogs] = await Promise.all([ + this._secureFile.list(), + this.noteDB.getAll(), + this.oplogDB.getAll(), + ]); + log("Fetched"); + // Create sync pairs (remote & local) + log("Building pairs"); + let pairs: { + local: DBNote; + remote: IFile; + id: string; + oplog: OpLog[]; + }[] = []; + remotes.map((r) => { + let lIdx = locals.findIndex((e) => e._id === r._id); + let l: DBNote = undefined; + if (lIdx >= 0) { + l = locals[lIdx]; + locals.splice(lIdx, 1); + } + + let oIdx = oplogs.findIndex((e) => e.id === r._id); + let oplog: OpLog[]; + if (oIdx >= 0) { + oplog = oplogs[oIdx].logs; + oplogs.splice(oIdx, 1); + } + + pairs.push({ + remote: r, + local: l, + oplog, + id: r._id, + }); + }); + + locals.forEach((l) => { + let oIdx = oplogs.findIndex((e) => e.id === l._id); + let oplog: OpLog[] = undefined; + if (oIdx >= 0) { + oplog = oplogs[oIdx].logs; + if (oplog.length <= 0) oplog = undefined; + oplogs.splice(oIdx, 1); + } + + pairs.push({ + remote: undefined, + local: l, + oplog, + id: l._id, + }); + }); + + oplogs.forEach((oplog) => { + if (!oplog) return; + if (oplog.logs.length > 0) + pairs.push({ + remote: undefined, + local: undefined, + oplog: oplog.logs, + id: oplog.id, + }); + }); + + log("Pairs builded"); + + let stats = { + remote_change: 0, + remote_delete: 0, + remote_create: 0, + local_change: 0, + local_delete: 0, + do_nothing: 0, + error: 0, + }; + + log("Start inspection"); + for (let { local, remote, oplog, id } of pairs) { + const apply = async (old = false) => { + log("Apply OPLOG to", id); + for (let op of oplog) { + switch (op.type) { + case OpLogType.CHANGE: + log(id, "REMOTE CHANGE"); + stats.remote_change++; + await this._secureFile.update( + id, + op.values.value, + b64.encode(op.values.preview), + op.date, + old + ); + break; + case OpLogType.DELETE: + log(id, "REMOTE DELETE"); + stats.remote_delete++; + if (old) break; // if the deletion is old, just ignore + await this._secureFile.delete(id); + break; + case OpLogType.CREATE: + log(id, "REMOTE CREATE"); + stats.remote_create++; + await this._secureFile.create( + "", + op.values.value, + "binary", + local.folder, + b64.encode(op.values.preview), + id, + op.date + ); + break; + } + } + }; + + const localChange = (id: string) => { + //TODO implement + }; + + const create = async () => { + log(id, "LOCAL CREATAE/UPDATE"); + stats.local_change++; + let value = await this._secureFile.get(id); + let note: DBNote = { + _id: remote._id, + folder: remote.folder, + preview: b64.decode(remote.active.preview || ""), + time: remote.active.time, + __value: new Uint8Array(value), + }; + await this.noteDB.set(id, note); + localChange(id); + }; + + try { + // log(id, "LRO: ", !!local, !!remote, !!oplog) + if (remote && !oplog) { + if (local) { + let old = + remote.active.time.valueOf() > local.time.valueOf(); + if (old) await create(); + else { + stats.do_nothing++; + log(id, "DO NOTHING"); + } + } else { + await create(); + } + } else if (!remote && local && !oplog) { + // No local changes, but remote deleted + log("LOCAL DELETE"); + stats.local_delete++; + await this.noteDB.delete(id); + localChange(id); + } else if (!remote && oplog) { + // Remote does not exist, but oplog, just apply all changes including possible delete + await apply(); + } else if (remote && oplog) { + let last = oplog[oplog.length - 1]; + let old = remote.active.time.valueOf() > last.date.valueOf(); + + if (old) await create(); // Will recreate local entry + await apply(old); // Will apply changes to remote + } else { + log(id, "DO NOTHING"); + stats.do_nothing++; + } + } catch (err) { + console.error(err); + stats.error++; + Notifications.sendNotification( + "Error syncing: " + id, + MessageType.ERROR + ); + } + await this.oplogDB.delete(id); + } + log("Stats", stats); + this.syncedObservableServer.send( + (await this.oplogDB.getAll()).length <= 0 + ); + } finally { + log("Finished"); + lock.release(); + this.syncObservableServer.send(false); + } + } + + public forgetVaultKey(vault_id: string) { + this.vaultKeys.delete(vault_id); + localStorage.removeItem("vault_" + vault_id); + } + + public getVaultKey(vault_id: string) { + let key = this.vaultKeys.get(vault_id); + if (!key) { + let lsk = localStorage.getItem("vault_" + vault_id); + if (lsk) { + key = b64.decode(lsk); + this.vaultKeys.set(vault_id, key); + } + } + return key; + } + + public saveVaultKey(vault_id: string, key: Uint8Array, permanent?: boolean) { + this.vaultKeys.set(vault_id, key); + if (permanent) { + localStorage.setItem("vault_" + vault_id, b64.encode(key)); + } + } + + public getVaults(): Promise { + return this.noteDB.getAll().then((notes) => + notes + .filter((e) => Decoder.decode(e.preview) === "__VAULT__") + .map((e) => { + let value = e.__value; + let encrypted = false; + if (this.decrypt(value) !== "__BASELINE__") encrypted = true; + return { name: e.folder, encrypted, id: e._id }; + }) + ); + } + + public async createVault(name: string, key?: Uint8Array) { + let vault: DBNote = { + __value: this.encrypt("__BASELINE__", key), + _id: uuidv4(), + folder: name, + preview: Encoder.encode("__VAULT__"), + time: new Date(), + }; + let tx = this.database.transaction(); + await Promise.all([ + this.addop( + vault._id, + OpLogType.CREATE, + { + value: vault.__value, + preview: vault.preview, + }, + vault.time, + tx + ), + this.noteDB.set(vault._id, vault), + ]); + } + + public async getVault(vault_id: string, key?: Uint8Array): Promise { + let vault = await this.noteDB.get(vault_id); + if (!vault) throw new Error("Vault not found!"); + if (this.decrypt(vault.__value, key) !== "__BASELINE__") + throw new Error("Invalid password!"); + return new NotesProvider.Vault(vault, key); + } + + public async deleteVault(vault_id: string) { + let vault = await this.noteDB.get(vault_id); + if (!vault) throw new Error("Vault not found!"); + let v = new NotesProvider.Vault(vault); + await Promise.all( + (await v.getAllNotes()).map((note) => this.delete(note._id)) + ); + await this.delete(v.id); // This can also delete a vault + } + + public passwordToKey(password: string) { + return new Uint8Array(sha256.arrayBuffer(password + config.client_id)); + } + + private _encrypt(value: Uint8Array, key?: Uint8Array): Uint8Array { + if (!key) return value; + var aesCtr = new aesjs.ModeOfOperation.ctr(key); + var encryptedBytes = aesCtr.encrypt(value); + return new Uint8Array(encryptedBytes); + } + + private encrypt(value: string, key?: Uint8Array): Uint8Array { + let msg = this._encrypt(Encoder.encode(value), key); + return new Uint8Array(this._encrypt(msg, this.generalEncryption)); + } + + private _decrypt(value: ArrayBuffer, key?: Uint8Array): Uint8Array { + if (!key) return new Uint8Array(value); + var aesCtr = new aesjs.ModeOfOperation.ctr(key); + var decryptedBytes = aesCtr.decrypt(value); + return new Uint8Array(decryptedBytes); + } + + private decrypt(value: ArrayBuffer, key?: Uint8Array): string { + let msg = this._decrypt(value, key); + return Decoder.decode(this._decrypt(msg, this.generalEncryption)); + } + + async addop( + note_id: string, + type: OpLogType, + values: { value: Uint8Array; preview: Uint8Array }, + date: Date, + transaction?: IDBPTransaction + ) { + let tx = transaction || this.oplogDB.transaction(); + let oplog = await this.oplogDB.get(note_id, tx); + if (!oplog) oplog = { logs: [], id: note_id }; + oplog.logs.push({ + date: date, + type, + values, + }); + this.syncedObservableServer.send(false); + await this.oplogDB.set(note_id, oplog, tx); + } + + async delete(id: string) { + let lock = await this.syncLock.getLock(); + let tx = this.database.transaction(this.oplogDB, this.noteDB); + await Promise.all([ + this.addop(id, OpLogType.DELETE, null, new Date(), tx), + this.noteDB.delete(id, tx), + ]); + lock.release(); + } + + static Vault = class implements IVault { + id: string; + name: string; + encrypted: boolean = false; + constructor(private vault: Note, private key?: Uint8Array) { + if (key) this.encrypted = true; + this.id = vault._id; + this.name = vault.folder; + } + + private encrypt(data: string) { + return Notes.encrypt(data, this.key); + } + + private decrypt(data: ArrayBuffer) { + return Notes.decrypt(data, this.key); + } + + async getAllNotes() { + return Notes.noteDB.getAll().then((all) => + all + .filter((e) => e.folder === this.vault._id) + .sort(this.sort) + .map((e) => { + let new_note = clonedeep(e) as BaseNote; + delete (new_note).__value; + new_note.preview = this.decrypt(e.preview); + return new_note; + }) + ); + } + + private sort(a: DBNote, b: DBNote) { + return new Date(b.time).getTime() - new Date(a.time).getTime(); + } + + async searchNote(term: string) { + let all = await this.getAllNotes(); + return all.filter((e) => e.preview.indexOf(term) >= 0); + } + + newNote(): ViewNote { + return { + _id: uuidv4(), + folder: this.vault._id, + time: new Date(), + __value: "", + preview: "", + }; + } + + async saveNote(note: ViewNote, date = new Date()) { + let lock = await Notes.syncLock.getLock(); + const tx = Notes.database.transaction(Notes.noteDB, Notes.oplogDB); + let old_note = await Notes.noteDB.get(note._id, tx); + + let new_note = clonedeep(note) as DBNote; + new_note.__value = this.encrypt(note.__value); + let [title, preview] = note.__value.split("\n"); + if (preview) preview = "\n" + preview; + else preview = ""; + new_note.preview = this.encrypt((title + preview).substr(0, 128)); + new_note.time = date; + + await Promise.all([ + Notes.addop( + note._id, + !old_note ? OpLogType.CREATE : OpLogType.CHANGE, + { + value: new_note.__value, + preview: new_note.preview, + }, + date, + tx + ), + Notes.noteDB.set(note._id, new_note, tx), + ]); + lock.release(); + } + + async getNote(id: string): Promise { + let note = await Notes.noteDB.get(id); + if (!note) return undefined; + let new_note = clonedeep(note) as ViewNote; + new_note.__value = this.decrypt(note.__value); + return new_note; + } + + deleteNote(id: string) { + return Notes.delete(id); + } + }; +} + +const Notes = new NotesProvider(); +export default Notes; + +(window).api = Notes;