diff --git a/apps/web/.gitignore b/apps/web/.gitignore
index 2ebc441cd..667b15af7 100644
--- a/apps/web/.gitignore
+++ b/apps/web/.gitignore
@@ -22,4 +22,6 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
-.now
\ No newline at end of file
+.now
+
+public/sodium.js
\ No newline at end of file
diff --git a/apps/web/package.json b/apps/web/package.json
index 7d025ba4f..12f9b81ac 100644
--- a/apps/web/package.json
+++ b/apps/web/package.json
@@ -13,7 +13,6 @@
"framer-motion": "^1.10.3",
"hookrouter": "^1.2.3",
"immer": "^6.0.3",
- "libsodium-wrappers": "^0.7.6",
"localforage": "^1.7.3",
"localforage-getitems": "https://github.com/thecodrr/localForage-getItems.git",
"notes-core": "npm:@streetwriters/notesnook-core@latest",
@@ -51,6 +50,7 @@
"debug": "BROWSER=none react-scripts start",
"test": "react-scripts test",
"eject": "react-scripts eject",
+ "sodium": "cd public && wget https://github.com/jedisct1/libsodium.js/raw/master/dist/browsers/sodium.js && cd ..",
"update": "npm i @streetwriters/editor@latest @streetwriters/notesnook-core@latest @streetwriters/theme@latest"
},
"eslintConfig": {
@@ -71,4 +71,4 @@
"last 4 edge version"
]
}
-}
+}
\ No newline at end of file
diff --git a/apps/web/public/crypto.worker.js b/apps/web/public/crypto.worker.js
new file mode 100644
index 000000000..eaed640e7
--- /dev/null
+++ b/apps/web/public/crypto.worker.js
@@ -0,0 +1,137 @@
+/* eslint-disable */
+
+var context;
+if (!self instanceof Window) {
+ self.sodium = {
+ onload: function (_sodium) {
+ context = { sodium: _sodium };
+ sendMessage("loaded");
+ },
+ };
+ importScripts("sodium.js");
+
+ self.addEventListener("message", onMessage);
+}
+
+function onMessage(ev) {
+ const { type, data, messageId } = ev.data;
+ if (type === "encrypt") {
+ const { passwordOrKey, data: _data } = data;
+ const cipher = encrypt.call(context, passwordOrKey, _data);
+ sendMessage("encrypt", cipher, messageId);
+ } else if (type === "decrypt") {
+ const { passwordOrKey, cipher } = data;
+ const plainText = decrypt.call(context, passwordOrKey, cipher);
+ sendMessage("decrypt", plainText, messageId);
+ } else if (type === "deriveKey") {
+ const { password, salt, exportKey } = data;
+ const derivedKey = deriveKey.call(context, password, salt, exportKey);
+ sendMessage("deriveKey", derivedKey, messageId);
+ }
+}
+
+function sendMessage(type, data, messageId) {
+ postMessage({ type, data, messageId });
+}
+
+const deriveKey = (password, salt, exportKey = false) => {
+ const { sodium } = this;
+
+ if (!salt) salt = sodium.randombytes_buf(sodium.crypto_pwhash_SALTBYTES);
+ else {
+ salt = sodium.from_base64(salt);
+ }
+
+ const key = sodium.crypto_pwhash(
+ sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
+ password,
+ salt,
+ 3, // operations limit
+ 1024 * 1024 * 8, // memory limit (8MB)
+ sodium.crypto_pwhash_ALG_ARGON2I13,
+ exportKey ? "base64" : "uint8array"
+ );
+ const saltHex = sodium.to_base64(salt);
+ sodium.memzero(salt);
+ if (exportKey) {
+ return key;
+ }
+ return { key, salt: saltHex };
+};
+
+const _getKey = (passwordOrKey) => {
+ const { sodium } = this;
+
+ let { salt, key, password } = passwordOrKey;
+ if (password) {
+ const result = deriveKey(password, salt);
+ key = result.key;
+ salt = result.salt;
+ } else if (key && salt) {
+ salt = passwordOrKey.salt;
+ key = sodium.from_base64(key);
+ }
+ return { key, salt };
+};
+
+/**
+ *
+ * @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
+ * @param {string|Object} data - the plaintext data
+ */
+const encrypt = (passwordOrKey, data) => {
+ const { sodium } = this;
+
+ const { key, salt } = _getKey(passwordOrKey);
+
+ const nonce = sodium.randombytes_buf(
+ sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
+ );
+ const cipher = sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
+ data,
+ undefined,
+ undefined,
+ nonce,
+ key,
+ "base64"
+ );
+ const iv = sodium.to_base64(nonce);
+ sodium.memzero(nonce);
+ sodium.memzero(key);
+ return {
+ cipher,
+ iv,
+ salt,
+ length: data.length,
+ };
+};
+
+/**
+ *
+ * @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
+ * @param {{salt: string, iv: string, cipher: string}} cipher - the cipher data
+ */
+const decrypt = (passwordOrKey, { iv, cipher, salt }) => {
+ const { sodium } = this;
+
+ const { key } = _getKey({ salt, ...passwordOrKey });
+
+ const plainText = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
+ undefined,
+ sodium.from_base64(cipher),
+ undefined,
+ sodium.from_base64(iv),
+ key,
+ "text"
+ );
+ sodium.memzero(key);
+ return plainText;
+};
+
+if (self instanceof Window) {
+ self.ncrypto = {
+ decrypt,
+ deriveKey,
+ encrypt,
+ };
+}
diff --git a/apps/web/src/components/editor/index.js b/apps/web/src/components/editor/index.js
index e58a5ab35..8f8966d44 100644
--- a/apps/web/src/components/editor/index.js
+++ b/apps/web/src/components/editor/index.js
@@ -3,7 +3,11 @@ import "./editor.css";
import ReactQuill from "./react-quill";
import { Flex } from "rebass";
import Properties from "../properties";
-import { useStore, SESSION_STATES } from "../../stores/editor-store";
+import {
+ useStore,
+ SESSION_STATES,
+ store as editorstore,
+} from "../../stores/editor-store";
import { useStore as useAppStore } from "../../stores/app-store";
import { useStore as useUserStore } from "../../stores/user-store";
import Animated from "../animated";
@@ -14,7 +18,7 @@ import SplitEditor from "../spliteditor";
import Unlock from "../unlock";
function Editor() {
- const delta = useStore((store) => store.session.content.delta);
+ console.log("rendering editor");
const sessionState = useStore((store) => store.session.state);
const setSession = useStore((store) => store.setSession);
const saveSession = useStore((store) => store.saveSession);
@@ -45,15 +49,21 @@ function Editor() {
useEffect(() => {
if (!quillRef || !quillRef.current) return;
+
if (sessionState === SESSION_STATES.new) {
+ const {
+ content: { delta },
+ } = editorstore.get().session;
+
const { quill } = quillRef.current;
quill.setContents(delta, "init");
quill.history.clear();
if (!delta.ops || !delta.ops.length) return;
const text = quill.getText();
quill.setSelection(text.length, 0, "init");
+ editorstore.set((state) => (state.session.state = SESSION_STATES.stale));
}
- }, [sessionState, delta, quillRef]);
+ }, [sessionState, quillRef]);
if (unlock) return ;
if (diff) return ;
@@ -84,17 +94,20 @@ function Editor() {
onFocus={() => {
toggleProperties(false);
}}
- initialContent={delta}
placeholder="Type anything here"
container=".editor"
onSave={() => {
saveSession();
}}
- onChange={(editor) => {
+ changeInterval={500}
+ onChange={() => {
+ const { quill } = quillRef.current;
+ const delta = quill.getContents().ops;
+ const text = quill.getText();
setSession((state) => {
state.session.content = {
- delta: { ops: editor.getContents().ops },
- text: editor.getText(),
+ delta: { ops: delta },
+ text: text,
};
});
}}
diff --git a/apps/web/src/components/editor/react-quill.js b/apps/web/src/components/editor/react-quill.js
index 0ae5d6fd4..702c9cd6d 100644
--- a/apps/web/src/components/editor/react-quill.js
+++ b/apps/web/src/components/editor/react-quill.js
@@ -53,6 +53,7 @@ const simpleQuillModules = {
export default class ReactQuill extends Component {
/** @private */
quill;
+ changeTimeout;
getEditor() {
return this.quill.editor;
}
@@ -108,9 +109,13 @@ export default class ReactQuill extends Component {
this.quill.off("text-change", this.textChangeHandler);
}
- textChangeHandler = (delta, oldDelta, source) => {
+ textChangeHandler = (_delta, _oldDelta, source) => {
if (source === "init") return;
- this.props.onChange(this.quill);
+ clearTimeout(this.changeTimeout);
+ this.changeTimeout = setTimeout(
+ this.props.onChange,
+ this.props.changeInterval
+ );
};
render() {
diff --git a/apps/web/src/interfaces/crypto.js b/apps/web/src/interfaces/crypto.js
index ef31bcc56..5c5faa827 100644
--- a/apps/web/src/interfaces/crypto.js
+++ b/apps/web/src/interfaces/crypto.js
@@ -5,51 +5,19 @@ class Crypto {
}
async _initialize() {
if (this.isReady) return;
- const { default: _sodium } = await import("libsodium-wrappers");
- await _sodium.ready;
- this.sodium = _sodium;
- this.isReady = true;
+ return new Promise(async (resolve) => {
+ window.sodium = {
+ onload: (_sodium) => {
+ if (this.isReady) return;
+ this.isReady = true;
+ this.sodium = _sodium;
+ loadScript("crypto.worker.js").then(resolve);
+ },
+ };
+ await loadScript("sodium.js");
+ });
}
- deriveKey = async (password, salt, exportKey = false) => {
- await this._initialize();
-
- if (!salt)
- salt = this.sodium.randombytes_buf(this.sodium.crypto_pwhash_SALTBYTES);
- else {
- salt = this.sodium.from_base64(salt);
- }
-
- const key = this.sodium.crypto_pwhash(
- this.sodium.crypto_aead_xchacha20poly1305_ietf_KEYBYTES,
- password,
- salt,
- 3, // operations limit
- 1024 * 1024 * 8, // memory limit (8MB)
- this.sodium.crypto_pwhash_ALG_ARGON2I13,
- exportKey ? "base64" : "uint8array"
- );
- const saltHex = this.sodium.to_base64(salt);
- this.sodium.memzero(salt);
- if (exportKey) {
- return key;
- }
- return { key, salt: saltHex };
- };
-
- _getKey = async (passwordOrKey) => {
- let { salt, key, password } = passwordOrKey;
- if (password) {
- const result = await this.deriveKey(password, salt);
- key = result.key;
- salt = result.salt;
- } else if (key && salt) {
- salt = passwordOrKey.salt;
- key = this.sodium.from_base64(key);
- }
- return { key, salt };
- };
-
/**
*
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
@@ -57,29 +25,7 @@ class Crypto {
*/
encrypt = async (passwordOrKey, data) => {
await this._initialize();
-
- const { key, salt } = await this._getKey(passwordOrKey);
-
- const nonce = this.sodium.randombytes_buf(
- this.sodium.crypto_aead_xchacha20poly1305_ietf_NPUBBYTES
- );
- const cipher = this.sodium.crypto_aead_xchacha20poly1305_ietf_encrypt(
- data,
- undefined,
- undefined,
- nonce,
- key,
- "base64"
- );
- const iv = this.sodium.to_base64(nonce);
- this.sodium.memzero(nonce);
- this.sodium.memzero(key);
- return {
- cipher,
- iv,
- salt,
- length: data.length,
- };
+ return global.ncrypto.encrypt.call(this, passwordOrKey, data);
};
/**
@@ -87,20 +33,100 @@ class Crypto {
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
* @param {{salt: string, iv: string, cipher: string}} cipher - the cipher data
*/
- decrypt = async (passwordOrKey, { iv, cipher, salt }) => {
+ decrypt = async (passwordOrKey, cipher) => {
await this._initialize();
- const { key } = await this._getKey({ salt, ...passwordOrKey });
+ return global.ncrypto.decrypt.call(this, passwordOrKey, cipher);
+ };
- const plainText = this.sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
- undefined,
- this.sodium.from_base64(cipher),
- undefined,
- this.sodium.from_base64(iv),
- key,
- "text"
- );
- this.sodium.memzero(key);
- return plainText;
+ deriveKey = async (password, salt, exportKey = false) => {
+ await this._initialize();
+ return global.ncrypto.deriveKey.call(this, password, salt, exportKey);
};
}
-export default Crypto;
+
+class CryptoWorker {
+ constructor() {
+ this.isReady = false;
+ }
+ async _initialize() {
+ if (this.isReady) return;
+ return new Promise((resolve) => {
+ this.worker = new Worker("crypto.worker.js");
+ this.worker.onmessage = (ev) => {
+ const { type } = ev.data;
+ if (type === "loaded") {
+ this.worker.onmessage = undefined;
+ this.isReady = true;
+ resolve(true);
+ }
+ };
+ });
+ }
+
+ _communicate(type, data) {
+ return new Promise(async (resolve) => {
+ await this._initialize();
+ const messageId = Math.random().toString(36).substr(2, 9);
+ const onMessage = (e) => {
+ const { type: _type, messageId: _mId } = e.data;
+ if (_type === type && _mId === messageId) {
+ this.worker.removeEventListener("message", onMessage);
+ resolve(e.data.data);
+ }
+ };
+ this.worker.addEventListener("message", onMessage);
+ this.worker.postMessage({
+ type,
+ data,
+ messageId,
+ });
+ });
+ }
+
+ /**
+ *
+ * @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
+ * @param {string|Object} data - the plaintext data
+ */
+ encrypt = (passwordOrKey, data) => {
+ return this._communicate("encrypt", {
+ passwordOrKey,
+ data,
+ });
+ };
+
+ /**
+ *
+ * @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
+ * @param {{salt: string, iv: string, cipher: string}} cipher - the cipher data
+ */
+ decrypt = (passwordOrKey, cipher) => {
+ return this._communicate("decrypt", { passwordOrKey, cipher });
+ };
+
+ deriveKey = (password, salt, exportKey = false) => {
+ return this._communicate("deriveKey", { password, salt, exportKey });
+ };
+}
+
+const NCrypto =
+ "Worker" in window || "Worker" in global ? CryptoWorker : Crypto;
+export default NCrypto;
+
+function loadScript(url) {
+ return new Promise((resolve) => {
+ // adding the script tag to the head as suggested before
+ var head = document.getElementsByTagName("head")[0];
+ var script = document.createElement("script");
+ script.type = "text/javascript";
+ script.src = url;
+
+ // then bind the event to the callback function
+ // there are several events for cross browser compatibility
+ script.onreadystatechange = resolve;
+ script.onload = resolve;
+
+ // fire the loading
+ head.appendChild(script);
+ });
+}
diff --git a/apps/web/src/stores/editor-store.js b/apps/web/src/stores/editor-store.js
index 29a5dcf88..9d44f31fe 100644
--- a/apps/web/src/stores/editor-store.js
+++ b/apps/web/src/stores/editor-store.js
@@ -13,7 +13,6 @@ const DEFAULT_SESSION = {
state: undefined,
isSaving: false,
title: "",
- timeout: 0,
id: "",
pinned: false,
favorite: false,
@@ -68,7 +67,6 @@ class EditorStore extends BaseStore {
let note = db.notes.note(noteId);
if (!note) return;
note = note.data;
- clearTimeout(this.get().session.timeout);
noteStore.setSelectedNote(note.id);
@@ -95,6 +93,7 @@ class EditorStore extends BaseStore {
saveSession = (oldSession) => {
this.set((state) => (state.session.isSaving = true));
+
this._saveFn()(this.get().session).then(async (id) => {
/* eslint-disable */
storeSync: if (oldSession) {
@@ -135,7 +134,6 @@ class EditorStore extends BaseStore {
newSession = (context = {}) => {
setHashParam({ note: 0 });
- clearTimeout(this.get().session.timeout);
this.set(function (state) {
state.session = {
...DEFAULT_SESSION,
@@ -149,7 +147,6 @@ class EditorStore extends BaseStore {
clearSession = () => {
setHashParam({});
- clearTimeout(this.get().session.timeout);
this.set(function (state) {
state.session = {
...DEFAULT_SESSION,
@@ -159,27 +156,10 @@ class EditorStore extends BaseStore {
noteStore.setSelectedNote(0);
};
- setSession = (set, immediate = false) => {
- clearTimeout(this.get().session.timeout);
+ setSession = (set) => {
const oldSession = { ...this.get().session };
-
- // TODO test this
- this.set((state) => {
- if (state.session.state !== SESSION_STATES.stale)
- state.session.state = SESSION_STATES.stale;
- });
-
- this.set((state) => {
- set(state);
-
- state.session.timeout = setTimeout(
- () => {
- this.session = this.get().session;
- this.saveSession(oldSession);
- },
- immediate ? 0 : 500
- );
- });
+ this.set(set);
+ this.saveSession(oldSession);
};
toggleLocked = () => {
@@ -224,11 +204,11 @@ class EditorStore extends BaseStore {
let index = arr.indexOf(value);
if (index > -1) {
note[`un${key}`](value).then(() => {
- this.setSession((state) => state.session[array].splice(index, 1), true);
+ this.setSession((state) => state.session[array].splice(index, 1));
});
} else {
note[key](value).then(() => {
- this.setSession((state) => state.session[array].push(value), true);
+ this.setSession((state) => state.session[array].push(value));
});
}
}
diff --git a/apps/web/yarn.lock b/apps/web/yarn.lock
index f650f99c4..92d36af8d 100644
--- a/apps/web/yarn.lock
+++ b/apps/web/yarn.lock
@@ -6951,18 +6951,6 @@ levn@^0.3.0, levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
-libsodium-wrappers@^0.7.6:
- version "0.7.8"
- resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.8.tgz#d95cdf3e7236c2aef76844bf8e1929ba9eef3e9e"
- integrity sha512-PDhPWXBqd/SaqAFUBgH2Ux7b3VEEJgyD6BQB+VdNFJb9PbExGr/T/myc/MBoSvl8qLzfm0W0IVByOQS5L1MrCg==
- dependencies:
- libsodium "0.7.8"
-
-libsodium@0.7.8:
- version "0.7.8"
- resolved "https://registry.yarnpkg.com/libsodium/-/libsodium-0.7.8.tgz#fbd12247b7b1353f88d8de1cbc66bc1a07b2e008"
- integrity sha512-/Qc+APf0jbeWSaeEruH0L1/tbbT+sbf884ZL0/zV/0JXaDPBzYkKbyb/wmxMHgAHzm3t6gqe7bOOXAVwfqVikQ==
-
lie@3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.1.1.tgz#9a436b2cc7746ca59de7a41fa469b3efb76bd87e"
@@ -7638,7 +7626,7 @@ normalize-url@^3.0.0, normalize-url@^3.0.1:
"notes-core@git+ssh://git@github.com:streetwriters/notesnook-core.git":
version "1.5.0"
- resolved "git+ssh://git@github.com:streetwriters/notesnook-core.git#fa55c0d6732cdfda5108a4e77071dc9e3cb89e70"
+ resolved "git+ssh://git@github.com:streetwriters/notesnook-core.git#24951bd7957f86c48ac303d831c1b36e1ad0df5d"
dependencies:
fast-sort "^2.0.1"
fuzzysearch "^1.0.3"