diff --git a/apps/web/package.json b/apps/web/package.json index 61a329f92..449191dec 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -10,6 +10,7 @@ "framer-motion": "^1.9.1", "git": "^0.1.5", "immer": "^5.3.6", + "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", diff --git a/apps/web/src/interfaces/crypto.js b/apps/web/src/interfaces/crypto.js new file mode 100644 index 000000000..337c60ae6 --- /dev/null +++ b/apps/web/src/interfaces/crypto.js @@ -0,0 +1,109 @@ +class Crypto { + isReady = false; + constructor() { + this.sodium = undefined; + } + async _initialize() { + if (this.isReady) return; + const _sodium = require("libsodium-wrappers"); + await _sodium.ready; + this.sodium = _sodium; + this.isReady = true; + } + + 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 ? "hex" : "uint8array" + ); + const saltHex = this.sodium.to_base64(salt); + this.sodium.memzero(salt); + if (exportKey) { + return key; + } + return { key, salt: saltHex }; + }; + + _getKey = async (passwordOrKey) => { + let key, salt; + if (passwordOrKey.password) { + const result = await this.deriveKey(passwordOrKey.password); + key = result.key; + salt = result.salt; + } else if (passwordOrKey.key && passwordOrKey.salt) { + salt = passwordOrKey.salt; + key = this.sodium.from_hex(passwordOrKey.key); + } + return { key, salt }; + }; + + /** + * + * @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key + * @param {string|Object} data - the plaintext data + */ + encrypt = async (passwordOrKey, data) => { + await this._initialize(); + + if (typeof data === "object") data = JSON.stringify(data); + + 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, + }; + }; + + /** + * + * @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 }) => { + await this._initialize(); + + const { key } = await this._getKey(passwordOrKey); + console.log(key, iv, cipher, passwordOrKey); + + 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; + }; +} +export default Crypto; diff --git a/apps/web/src/interfaces/storage.js b/apps/web/src/interfaces/storage.js index c1d52d927..af876f458 100644 --- a/apps/web/src/interfaces/storage.js +++ b/apps/web/src/interfaces/storage.js @@ -1,7 +1,9 @@ import localforage from "localforage"; import { extendPrototype } from "localforage-getitems"; import sort from "fast-sort"; +import Crypto from "./crypto"; +const crypto = new Crypto(); extendPrototype(localforage); localforage.config({ @@ -36,4 +38,7 @@ export default { write, remove, clear, + deriveKey: crypto.deriveKey, + encrypt: crypto.encrypt, + decrypt: crypto.decrypt, }; diff --git a/apps/web/yarn.lock b/apps/web/yarn.lock index b84de0141..c7d2d4974 100644 --- a/apps/web/yarn.lock +++ b/apps/web/yarn.lock @@ -6268,7 +6268,7 @@ levn@^0.3.0, levn@~0.3.0: prelude-ls "~1.1.2" type-check "~0.3.2" -libsodium-wrappers@^0.7.6: +libsodium-wrappers@0.7.6, libsodium-wrappers@^0.7.6: version "0.7.6" resolved "https://registry.yarnpkg.com/libsodium-wrappers/-/libsodium-wrappers-0.7.6.tgz#baed4c16d4bf9610104875ad8a8e164d259d48fb" integrity sha512-OUO2CWW5bHdLr6hkKLHIKI4raEkZrf3QHkhXsJ1yCh6MZ3JDA7jFD3kCATNquuGSG6MjjPHQIQms0y0gBDzjQg== @@ -6910,7 +6910,7 @@ normalize-url@^3.0.0, normalize-url@^3.0.1: "notes-core@git+ssh://git@github.com:thecodrr/notes-core.git": version "1.2.0" - resolved "git+ssh://git@github.com:thecodrr/notes-core.git#47dcb328be59c040c7ea50a6e1f26a2572ee1270" + resolved "git+ssh://git@github.com:thecodrr/notes-core.git#d0e226b0ba97a44f7098955ee6d982904a51be9d" dependencies: fast-sort "^2.0.1" fuzzysearch "^1.0.3"