feat: initial work

This commit is contained in:
thecodrr
2021-09-15 02:16:27 +05:00
parent 624a40d1b9
commit 7dfb12d2e1
10 changed files with 327 additions and 29 deletions

View File

@@ -9,9 +9,9 @@
"dependencies": { "dependencies": {
"@mdi/js": "^5.9.55", "@mdi/js": "^5.9.55",
"@mdi/react": "^1.4.0", "@mdi/react": "^1.4.0",
"@notesnook/desktop": "./desktop/", "@notesnook/desktop": "file:./desktop/",
"@rebass/forms": "^4.0.6", "@rebass/forms": "^4.0.6",
"@streetwritersco/tinymce-plugins": "^1.1.2", "@streetwritersco/tinymce-plugins": "file:../notesnook/packages/tinymce-plugins",
"@tinymce/tinymce-react": "^3.12.6", "@tinymce/tinymce-react": "^3.12.6",
"clipboard": "^2.0.6", "clipboard": "^2.0.6",
"cogo-toast": "^4.2.3", "cogo-toast": "^4.2.3",
@@ -48,6 +48,7 @@
"tinymce": "5.8.1", "tinymce": "5.8.1",
"uzip": "^0.20201231.0", "uzip": "^0.20201231.0",
"wouter": "^2.7.3", "wouter": "^2.7.3",
"xxhash-wasm": "^0.4.2",
"zustand": "^3.3.1" "zustand": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -12,10 +12,21 @@ function onMessage(ev) {
switch (type) { switch (type) {
case "encrypt": { case "encrypt": {
const { passwordOrKey, data: _data } = data; const { passwordOrKey, data: _data } = data;
const cipher = encrypt.call(context, passwordOrKey, _data); const cipher = encrypt.call(context, passwordOrKey, _data, "base64");
sendMessage("encrypt", cipher, messageId); sendMessage("encrypt", cipher, messageId);
break; break;
} }
case "encryptBinary": {
const { passwordOrKey, data: _data } = data;
const cipher = encrypt.call(
context,
passwordOrKey,
_data,
"uint8array"
);
sendMessage("encryptBinary", cipher, messageId, [cipher.cipher.buffer]);
break;
}
case "decrypt": { case "decrypt": {
const { passwordOrKey, cipher } = data; const { passwordOrKey, cipher } = data;
const plainText = decrypt.call(context, passwordOrKey, cipher); const plainText = decrypt.call(context, passwordOrKey, cipher);
@@ -125,7 +136,7 @@ const _getKey = (passwordOrKey) => {
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key * @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
* @param {{type: "plain" | "uint8array", data: string | Uint8Array}} plainData - the plaintext data * @param {{type: "plain" | "uint8array", data: string | Uint8Array}} plainData - the plaintext data
*/ */
const encrypt = (passwordOrKey, plainData) => { const encrypt = (passwordOrKey, plainData, outputType) => {
const { sodium } = this; const { sodium } = this;
if (plainData.type === "plain") { if (plainData.type === "plain") {
@@ -149,7 +160,7 @@ const encrypt = (passwordOrKey, plainData) => {
undefined, undefined,
nonce, nonce,
key, key,
"base64" outputType
); );
const iv = sodium.to_base64(nonce); const iv = sodium.to_base64(nonce);
sodium.memzero(nonce); sodium.memzero(nonce);
@@ -171,14 +182,20 @@ const encrypt = (passwordOrKey, plainData) => {
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key * @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
* @param {{salt: string, iv: string, cipher: string}} cipher - the cipher data * @param {{salt: string, iv: string, cipher: string}} cipher - the cipher data
*/ */
const decrypt = (passwordOrKey, { iv, cipher, salt, output }) => { const decrypt = (passwordOrKey, { iv, cipher, salt, output, inputType }) => {
const { sodium } = this; const { sodium } = this;
const { key } = _getKey({ salt, ...passwordOrKey }); const { key } = _getKey({ salt, ...passwordOrKey });
const input =
inputType === "uint8array"
? input
: inputType === "base64"
? sodium.from_base64(cipher)
: sodium.decode(cipher);
const data = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt( const data = sodium.crypto_aead_xchacha20poly1305_ietf_decrypt(
undefined, undefined,
sodium.from_base64(cipher), input,
undefined, undefined,
sodium.from_base64(iv), sodium.from_base64(iv),
key, key,

View File

@@ -6,7 +6,7 @@
/security#csp-meta-tag --> /security#csp-meta-tag -->
<meta <meta
http-equiv="Content-Security-Policy" http-equiv="Content-Security-Policy"
content="script-src 'self' https://analytics.streetwriters.co https://cdn.paddle.com 'unsafe-inline';" content="script-src 'self' https://analytics.streetwriters.co https://cdn.paddle.com 'unsafe-inline' 'unsafe-eval';"
/> />
<link <link
rel="apple-touch-icon" rel="apple-touch-icon"

View File

@@ -1,4 +1,5 @@
import StorageInterface from "../interfaces/storage"; import StorageInterface from "../interfaces/storage";
import FS from "../interfaces/fs";
import EventSource from "eventsource"; import EventSource from "eventsource";
import Config from "../utils/config"; import Config from "../utils/config";
import http from "notes-core/utils/http"; import http from "notes-core/utils/http";
@@ -12,13 +13,8 @@ import { isTesting } from "../utils/platform";
var db; var db;
async function initializeDatabase() { async function initializeDatabase() {
const { default: Database } = await import("notes-core/api"); const { default: Database } = await import("notes-core/api");
db = new Database(StorageInterface, EventSource); db = new Database(StorageInterface, EventSource, FS);
// db.host({
// API_HOST: "http://localhost:5264",
// AUTH_HOST: "http://localhost:8264",
// SSE_HOST: "http://localhost:7264",
// });
if (isTesting()) { if (isTesting()) {
db.host({ db.host({
API_HOST: "https://api.notesnook.com", API_HOST: "https://api.notesnook.com",
@@ -27,9 +23,9 @@ async function initializeDatabase() {
}); });
} else { } else {
db.host({ db.host({
API_HOST: "http://192.168.10.23:5264", API_HOST: "http://localhost:5264",
AUTH_HOST: "http://192.168.10.23:8264", AUTH_HOST: "http://localhost:8264",
SSE_HOST: "http://192.168.10.23:7264", SSE_HOST: "http://localhost:7264",
}); });
} }

View File

@@ -0,0 +1,146 @@
import Compressor from "compressorjs";
import { db } from "../../../common/db";
import fs from "../../../interfaces/fs";
function register(editor) {
editor.ui.registry.addButton("attachment", {
icon: "attachment",
tooltip: "Attach a file",
onAction: () => insertFile(editor),
});
editor.ui.registry.addButton("image", {
icon: "image",
tooltip: "Insert image",
onAction: () => insertImage(editor),
});
editor.addCommand("InsertImage", function () {
insertImage(editor);
});
}
async function insertImage(editor) {
const image = await pickImage();
if (!image) return;
var content = `<img class="attachment" data-mime="${image.type}" data-hash="${image.hash}" data-filename="${image.filename}" src="${image.dataurl}" data-size="${image.size}"/>`;
editor.insertContent(content);
}
async function insertFile(editor) {
const file = await pickFile();
if (!file) return;
var content = `<span class="attachment" data-mime="${file.type}" data-filename="${file.filename}" data-hash="${file.hash}" data-size="${file.size}"/>`;
editor.insertContent(content);
}
(function init() {
global.tinymce.PluginManager.add("attachmentpicker", register);
})();
async function pickFile() {
const selectedFile = await showFilePicker({ acceptedFileTypes: "*/*" });
if (!selectedFile) return;
const key = await getEncryptionKey();
const buffer = await selectedFile.arrayBuffer();
const output = await fs.writeEncrypted(null, {
data: buffer,
type: "buffer",
key,
});
await db.attachments.add({
...output,
filename: selectedFile.name,
type: selectedFile.type,
});
return {
hash: selectedFile.hash,
filename: selectedFile.name,
type: selectedFile.type,
size: selectedFile.size,
};
}
async function pickImage() {
const selectedImage = await showFilePicker({ acceptedFileTypes: "image/*" });
if (!selectedImage) return;
const { dataurl, buffer } = await compressImage(selectedImage, "buffer");
const key = await getEncryptionKey();
const output = await fs.writeEncrypted(null, {
data: buffer,
type: "buffer",
key,
});
await db.attachments.add({
...output,
filename: selectedImage.name,
type: selectedImage.type,
});
return {
hash: selectedImage.hash,
filename: selectedImage.name,
type: selectedImage.type,
size: output.length,
dataurl,
};
}
async function getEncryptionKey() {
const key = await db.user.getEncryptionKey();
if (!key) throw new Error("No encryption key found. Are you logged in?");
return key;
}
/**
*
* @returns {Promise<File>}
*/
function showFilePicker({ acceptedFileTypes }) {
return new Promise((resolve) => {
const input = document.createElement("input");
input.setAttribute("type", "file");
input.setAttribute("accept", acceptedFileTypes);
input.dispatchEvent(new MouseEvent("click"));
input.onchange = async function () {
var file = this.files[0];
if (!file) return null;
resolve(file);
};
});
}
/**
*
* @param {File} file
* @param {"base64"|"buffer"} type
*/
function compressImage(file, type) {
return new Promise((resolve, reject) => {
new Compressor(file, {
quality: 0.8,
mimeType: file.type,
width: 1920,
/**
*
* @param {Blob} result
*/
async success(result) {
const buffer = await result.arrayBuffer();
const base64 = Buffer.from(buffer).toString("base64");
resolve({ dataurl: `data:${file.type};base64,${base64}`, buffer });
},
error(err) {
reject(err);
},
});
});
}

View File

@@ -0,0 +1,16 @@
import * as Icons from "@mdi/js";
function register(editor) {
editor.ui.registry.addIcon(
"attachment",
createSvgElement(Icons.mdiAttachment)
);
}
function createSvgElement(path) {
return `<svg height="24" width="24"><path d="${path}" /></svg>`;
}
(function init() {
global.tinymce.PluginManager.add("icons", register);
})();

View File

@@ -25,12 +25,13 @@ import "tinymce/plugins/media";
import { processPastedContent } from "@streetwritersco/tinymce-plugins/codeblock"; import { processPastedContent } from "@streetwritersco/tinymce-plugins/codeblock";
import "@streetwritersco/tinymce-plugins/inlinecode"; import "@streetwritersco/tinymce-plugins/inlinecode";
import "@streetwritersco/tinymce-plugins/shortlink"; import "@streetwritersco/tinymce-plugins/shortlink";
import "@streetwritersco/tinymce-plugins/quickimage";
import "@streetwritersco/tinymce-plugins/checklist"; import "@streetwritersco/tinymce-plugins/checklist";
import "@streetwritersco/tinymce-plugins/collapsibleheaders"; import "@streetwritersco/tinymce-plugins/collapsibleheaders";
import "@streetwritersco/tinymce-plugins/paste"; import "@streetwritersco/tinymce-plugins/paste";
import "@streetwritersco/tinymce-plugins/shortcuts"; import "@streetwritersco/tinymce-plugins/shortcuts";
import "@streetwritersco/tinymce-plugins/keyboardquirks"; import "@streetwritersco/tinymce-plugins/keyboardquirks";
import "./plugins/attachmentpicker";
import "./plugins/icons";
import { Editor } from "@tinymce/tinymce-react"; import { Editor } from "@tinymce/tinymce-react";
import { showBuyDialog } from "../../common/dialog-controller"; import { showBuyDialog } from "../../common/dialog-controller";
import { useStore as useThemeStore } from "../../stores/theme-store"; import { useStore as useThemeStore } from "../../stores/theme-store";
@@ -109,7 +110,7 @@ const plugins = {
default: default:
"importcss searchreplace autolink directionality media table hr advlist lists imagetools noneditable quickbars autoresize", "importcss searchreplace autolink directionality media table hr advlist lists imagetools noneditable quickbars autoresize",
custom: custom:
"keyboardquirks collapsibleheaders shortlink quickimage paste codeblock inlinecode shortcuts checklist", "icons keyboardquirks collapsibleheaders shortlink attachmentpicker paste codeblock inlinecode shortcuts checklist",
pro: "textpattern", pro: "textpattern",
}; };
@@ -200,7 +201,7 @@ function TinyMCE(props) {
`, `,
toolbar: simple toolbar: simple
? false ? false
: `bold italic underline strikethrough inlinecode | blockquote codeblock | fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent subscript superscript | numlist bullist checklist | forecolor backcolor removeformat | hr | image media link table | ltr rtl | searchreplace`, : `bold italic underline strikethrough inlinecode | blockquote codeblock | fontsizeselect formatselect | alignleft aligncenter alignright alignjustify | outdent indent subscript superscript | numlist bullist checklist | forecolor backcolor removeformat | hr | image attachment media link table | ltr rtl | searchreplace`,
quickbars_selection_toolbar: false, quickbars_selection_toolbar: false,
mobile: { mobile: {
toolbar_mode: "scrolling", toolbar_mode: "scrolling",

View File

@@ -0,0 +1,68 @@
import NNCrypto from "./nncrypto/index";
import localforage from "localforage";
import xxhash from "xxhash-wasm";
const crypto = new NNCrypto();
const fs = localforage.createInstance({
storeName: "notesnook-fs",
name: "NotesnookFS",
driver: [localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE],
});
fs.hasItem = async function (key) {
const keys = await fs.keys();
return keys.includes(key);
};
/**
* We perform 4 steps here:
* 1. We convert base64 to Uint8Array (if we get base64, that is)
* 2. We hash the Uint8Array.
* 3. We encrypt the Uint8Array
* 4. We save the encrypted Uint8Array
*/
async function writeEncrypted(filename, { data, type, key }) {
const saveAsBuffer = localforage.supports(localforage.INDEXEDDB);
if (type === "base64") data = new Uint8Array(Buffer.from(data, "base64"));
const { hash, type: hashType } = await hashBuffer(data);
if (!filename) filename = hash;
if (await fs.hasItem(filename)) return { hash, hashType };
const output = saveAsBuffer
? await crypto.encryptBinary(key, data, "buffer")
: await crypto.encrypt(key, data, "buffer");
await fs.setItem(filename, output.cipher);
return {
hash,
hashType,
iv: output.iv,
length: output.length,
salt: output.salt,
alg: output.alg,
};
}
async function hashBuffer(data) {
const hasher = await xxhash();
return {
hash: Buffer.from(hasher.h64Raw(data)).toString("base64"),
type: "xxh64",
};
}
async function readEncrypted(filename, key, cipherData) {
const readAsBuffer = localforage.supports(localforage.INDEXEDDB);
cipherData.cipher = await fs.getItem(filename);
if (!cipherData.cipher)
throw new Error(`File not found. Filename: ${filename}`);
return readAsBuffer
? await crypto.decryptBinary(key, cipherData, cipherData.outputType)
: await crypto.decrypt(key, cipherData, cipherData.outputType);
}
const FS = { writeEncrypted, readEncrypted };
export default FS;

View File

@@ -24,10 +24,24 @@ export default class NNCrypto {
* @param {string} plainData - the plaintext data * @param {string} plainData - the plaintext data
* @param {boolean} * @param {boolean}
*/ */
encrypt = async (passwordOrKey, plainData) => { encrypt = async (passwordOrKey, plainData, type = "plain") => {
await this._initialize(); await this._initialize();
return global.ncrypto.encrypt.call(this, passwordOrKey, { return global.ncrypto.encrypt.call(this, passwordOrKey, {
type: "plain", type,
data: plainData,
});
};
/**
*
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
* @param {string} plainData - the plaintext data
* @param {string} type
*/
encryptBinary = async (passwordOrKey, plainData, type = "plain") => {
await this._initialize();
return global.ncrypto.encryptBinary.call(this, passwordOrKey, {
type,
data: plainData, data: plainData,
}); });
}; };

View File

@@ -65,11 +65,49 @@ export default class NNCryptoWorker {
* @param {string} data - the plaintext data * @param {string} data - the plaintext data
* @param {boolean} compress * @param {boolean} compress
*/ */
encrypt = (passwordOrKey, data) => { encrypt = (passwordOrKey, data, type = "plain") => {
return this._communicate("encrypt", { const payload = { type, data };
passwordOrKey, const transferables = type === "buffer" ? [payload.data] : [];
data: { type: "plain", data }, return this._communicate(
}); "encrypt",
{
passwordOrKey,
data: payload,
},
transferables
);
};
/**
*
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
* @param {string} data - the plaintext data
* @param {string} type
*/
encryptBinary = (passwordOrKey, data, type = "plain") => {
const payload = { type, data };
const transferables = type === "buffer" ? [payload.data] : [];
return this._communicate(
"encryptBinary",
{
passwordOrKey,
data: payload,
},
transferables
);
};
/**
*
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
* @param {{alg: string, salt: string, iv: string, cipher: Uint8Array}} cipherData - the cipher data
*/
decryptBinary = (passwordOrKey, cipherData, outputType = "text") => {
cipherData.output = outputType;
cipherData.inputType = "uint8array";
return this._communicate("decrypt", { passwordOrKey, cipher: cipherData }, [
cipherData.cipher,
]);
}; };
/** /**
@@ -77,8 +115,9 @@ export default class NNCryptoWorker {
* @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key * @param {{password: string}|{key:string, salt: string}} passwordOrKey - password or derived key
* @param {{alg: string, salt: string, iv: string, cipher: string}} cipherData - the cipher data * @param {{alg: string, salt: string, iv: string, cipher: string}} cipherData - the cipher data
*/ */
decrypt = (passwordOrKey, cipherData) => { decrypt = (passwordOrKey, cipherData, outputType = "text") => {
cipherData.output = "text"; cipherData.output = outputType;
cipherData.inputType = "base64";
return this._communicate("decrypt", { passwordOrKey, cipher: cipherData }); return this._communicate("decrypt", { passwordOrKey, cipher: cipherData });
}; };