From 2609d73bbda0139a3dd19e5dec681a774ba1ef03 Mon Sep 17 00:00:00 2001 From: nicwands Date: Mon, 2 Mar 2026 11:59:52 -0500 Subject: [PATCH] add encryption --- bin/generateEncryptionKey.js | 3 + out/main/index.js | 93 ++++++++++++++++++---- package-lock.json | 149 +++++++---------------------------- package.json | 5 +- src/main/core/NotesAPI.js | 93 ++++++++++++++++++++-- src/main/index.js | 19 +++-- 6 files changed, 208 insertions(+), 154 deletions(-) create mode 100644 bin/generateEncryptionKey.js diff --git a/bin/generateEncryptionKey.js b/bin/generateEncryptionKey.js new file mode 100644 index 0000000..6868c53 --- /dev/null +++ b/bin/generateEncryptionKey.js @@ -0,0 +1,3 @@ +import crypto from 'crypto' + +console.log(crypto.randomBytes(32).toString('hex')) diff --git a/out/main/index.js b/out/main/index.js index 0c6d035..558ec34 100644 --- a/out/main/index.js +++ b/out/main/index.js @@ -7,6 +7,7 @@ import fs from "fs/promises"; import path, { join } from "path"; import { Index } from "flexsearch"; import crypto from "crypto"; +import sodium from "libsodium-wrappers"; import __cjs_mod__ from "node:module"; const __filename = import.meta.filename; const __dirname = import.meta.dirname; @@ -88,23 +89,73 @@ class PluginConfig { } } class NotesAPI { - constructor(adapter) { + constructor(adapter, encryptionKey = null) { if (!adapter) { throw new Error("NotesAPI requires a storage adapter"); } this.adapter = adapter; this.notesCache = /* @__PURE__ */ new Map(); + this.encryptionKey = encryptionKey || process.env.NOTES_ENCRYPTION_KEY; + this._sodiumReady = false; this.index = new Index({ tokenize: "tolerant", resolution: 9 }); } + async _initSodium() { + if (!this._sodiumReady) { + await sodium.ready; + this._sodiumReady = true; + } + } + _encrypt(note) { + if (!this.encryptionKey) { + throw new Error("Encryption key not set"); + } + const key = Buffer.from(this.encryptionKey, "hex"); + if (key.length !== 32) { + throw new Error("Encryption key must be 64 hex characters (32 bytes)"); + } + const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES); + const message = JSON.stringify(note); + const ciphertext = sodium.crypto_secretbox_easy( + Buffer.from(message), + nonce, + key + ); + const combined = Buffer.concat([nonce, ciphertext]); + return combined.toString("base64"); + } + _decrypt(encryptedData) { + if (!this.encryptionKey) { + throw new Error("Encryption key not set"); + } + const key = Buffer.from(this.encryptionKey, "hex"); + if (key.length !== 32) { + throw new Error("Encryption key must be 64 hex characters (32 bytes)"); + } + const combined = Buffer.from(encryptedData, "base64"); + const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES); + const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES); + const decrypted = sodium.crypto_secretbox_open_easy( + ciphertext, + nonce, + key + ); + return JSON.parse(decrypted.toString()); + } async init() { + await this._initSodium(); await this.adapter.init(); - const notes = await this.adapter.getAll(); - for (const note of notes) { - this.notesCache.set(note.id, note); - this.index.add(note.id, note.title + "\n" + note.content); + const encryptedNotes = await this.adapter.getAll(); + for (const encryptedNote of encryptedNotes) { + try { + const note = this._decrypt(encryptedNote.data || encryptedNote); + this.notesCache.set(note.id, note); + this.index.add(note.id, note.title + "\n" + note.content); + } catch (error) { + console.error("Failed to decrypt note:", error); + } } } /* ----------------------- @@ -136,9 +187,13 @@ class NotesAPI { updatedAt: now, content }; + const encryptedNote = { + id: note.id, + data: this._encrypt(note) + }; this.notesCache.set(id, note); this.index.add(id, note.title + "\n" + content); - await this.adapter.create(note); + await this.adapter.create(encryptedNote); return note; } async deleteNote(id) { @@ -151,8 +206,12 @@ class NotesAPI { if (!note) throw new Error("Note not found"); note.content = content; note.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); + const encryptedNote = { + id: note.id, + data: this._encrypt(note) + }; this.index.update(id, note.title + "\n" + content); - await this.adapter.update(note); + await this.adapter.update(encryptedNote); return note; } async updateNoteMetadata(id, updates = {}) { @@ -171,8 +230,12 @@ class NotesAPI { note.category = updates.category; } note.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); + const encryptedNote = { + id: note.id, + data: this._encrypt(note) + }; this.index.update(id, note.title + "\n" + note.content); - await this.adapter.update(note); + await this.adapter.update(encryptedNote); return note; } search(query) { @@ -235,13 +298,13 @@ app.whenReady().then(async () => { const registry = new PluginRegistry(); registry.register(filesystemPlugin); registry.register(supabasePlugin); - await new PluginConfig(filesystemPlugin).load(); - const plugin = registry.get(supabasePlugin.id); - const adapter = plugin.createAdapter({ - supabaseKey: process.env.SUPABASE_KEY, - supabaseUrl: process.env.SUPABASE_URL - }); - const notesAPI = new NotesAPI(adapter); + const config = await new PluginConfig(filesystemPlugin).load(); + const plugin = registry.get(config.activeAdapter); + const adapter = plugin.createAdapter(config.adapterConfig); + const notesAPI = new NotesAPI( + adapter, + "729a0d21d783654c68f1a0123e2a0e986350de536b5324f1f35876ea12ffeaf5" + ); await notesAPI.init(); ipcMain.handle("notesAPI:call", (_, method, args) => { if (!notesAPI[method]) { diff --git a/package-lock.json b/package-lock.json index d55ab9f..2d31773 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,18 +1,18 @@ { - "name": "app.takerofnotes.com", - "version": "1.0.0", + "name": "takerofnotes-app", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "app.takerofnotes.com", - "version": "1.0.0", + "name": "takerofnotes-app", + "version": "0.1.0", "hasInstallScript": true, "dependencies": { "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", "@fuzzco/font-loader": "^1.0.2", - "@takerofnotes/plugin-filesystem": "^0.1.1", + "@takerofnotes/plugin-filesystem": "^0.2.0", "@takerofnotes/plugin-supabase": "^0.1.0", "@tiptap/extension-document": "^3.19.0", "@tiptap/extension-image": "^3.19.0", @@ -27,6 +27,7 @@ "flexsearch": "^0.8.212", "gsap": "^3.14.2", "lenis": "^1.3.17", + "libsodium-wrappers": "^0.8.2", "lodash": "^4.17.23", "sass": "^1.97.3", "sass-embedded": "^1.97.3", @@ -2343,19 +2344,18 @@ } }, "node_modules/@takerofnotes/plugin-filesystem": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@takerofnotes/plugin-filesystem/-/plugin-filesystem-0.1.1.tgz", - "integrity": "sha512-u3L6HLxN/+t7PTtzzRA2uzgaVh/O1vasngU/tQeC6JsTnlIrypWFVbAlTS5zT9Km4y+8w6C16eEq7tcN8+5lPg==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@takerofnotes/plugin-filesystem/-/plugin-filesystem-0.2.0.tgz", + "integrity": "sha512-BP7HBN0SKAqBiv5pDtXpyVmkW9UrOnPXpKeThTwQTIShHyN5aPaD8ZEyv8+vfTSs3qnXWGplNdEPVjbQmc27+Q==", "license": "MIT", "dependencies": { - "@takerofnotes/plugin-sdk": "^0.1.0", - "gray-matter": "^4.0.3" + "@takerofnotes/plugin-sdk": "^0.3.1" } }, "node_modules/@takerofnotes/plugin-sdk": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@takerofnotes/plugin-sdk/-/plugin-sdk-0.1.0.tgz", - "integrity": "sha512-ofhwwiQ59kNMEg2vvYoNq5JdXHB9/6TkDsbyroM5nsP/VPUvJSQ5g0UWCYBhteSzJ36iFUB+LtUwVt6gOXDClw==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@takerofnotes/plugin-sdk/-/plugin-sdk-0.3.1.tgz", + "integrity": "sha512-9GfPKyu1n52N00zYlLK32wdmGdc2uSd0jTj6UEixauW0TXn/7hD6SLpLRGSXcfZOJGGoi3iQk4MfjNsthe2ucw==", "license": "MIT", "dependencies": { "zod": "^4.3.6" @@ -5105,19 +5105,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/estree-walker": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", @@ -5137,18 +5124,6 @@ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "license": "MIT" }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", @@ -5625,49 +5600,6 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/gray-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.2", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", - "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/gray-matter/node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, "node_modules/gsap": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/gsap/-/gsap-3.14.2.tgz", @@ -5930,15 +5862,6 @@ "node": ">= 12" } }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -6155,15 +6078,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/lazy-val": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz", @@ -6196,6 +6110,21 @@ } } }, + "node_modules/libsodium": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium/-/libsodium-0.8.2.tgz", + "integrity": "sha512-TsnGYMoZtpweT+kR+lOv5TVsnJ/9U0FZOsLFzFOMWmxqOAYXjX3fsrPAW+i1LthgDKXJnI9A8dWEanT1tnJKIw==", + "license": "ISC" + }, + "node_modules/libsodium-wrappers": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/libsodium-wrappers/-/libsodium-wrappers-0.8.2.tgz", + "integrity": "sha512-VFLmfxkxo+U9q60tjcnSomQBRx2UzlRjKWJqvB4K1pUqsMQg4cu3QXA2nrcsj9A1qRsnJBbi2Ozx1hsiDoCkhw==", + "license": "ISC", + "dependencies": { + "libsodium": "^0.8.0" + } + }, "node_modules/linkify-it": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", @@ -8121,19 +8050,6 @@ "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", "license": "MIT" }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -8416,15 +8332,6 @@ "node": ">=8" } }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/sumchecker": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sumchecker/-/sumchecker-3.0.1.tgz", diff --git a/package.json b/package.json index b3ceb7a..875f68a 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "app.takerofnotes.com", + "name": "takerofnotes-app", "version": "0.1.0", "description": "An Electron application with Vue", "main": "./out/main/index.js", @@ -28,7 +28,7 @@ "@electron-toolkit/preload": "^3.0.2", "@electron-toolkit/utils": "^4.0.0", "@fuzzco/font-loader": "^1.0.2", - "@takerofnotes/plugin-filesystem": "^0.1.1", + "@takerofnotes/plugin-filesystem": "^0.2.0", "@takerofnotes/plugin-supabase": "^0.1.0", "@tiptap/extension-document": "^3.19.0", "@tiptap/extension-image": "^3.19.0", @@ -43,6 +43,7 @@ "flexsearch": "^0.8.212", "gsap": "^3.14.2", "lenis": "^1.3.17", + "libsodium-wrappers": "^0.8.2", "lodash": "^4.17.23", "sass": "^1.97.3", "sass-embedded": "^1.97.3", diff --git a/src/main/core/NotesAPI.js b/src/main/core/NotesAPI.js index de15733..05b4e5f 100644 --- a/src/main/core/NotesAPI.js +++ b/src/main/core/NotesAPI.js @@ -1,14 +1,17 @@ import { Index } from 'flexsearch' import crypto from 'crypto' +import sodium from 'libsodium-wrappers' export default class NotesAPI { - constructor(adapter) { + constructor(adapter, encryptionKey = null) { if (!adapter) { throw new Error('NotesAPI requires a storage adapter') } this.adapter = adapter this.notesCache = new Map() + this.encryptionKey = encryptionKey || process.env.NOTES_ENCRYPTION_KEY + this._sodiumReady = false this.index = new Index({ tokenize: 'tolerant', @@ -16,14 +19,73 @@ export default class NotesAPI { }) } + async _initSodium() { + if (!this._sodiumReady) { + await sodium.ready + this._sodiumReady = true + } + } + + _encrypt(note) { + if (!this.encryptionKey) { + throw new Error('Encryption key not set') + } + + const key = Buffer.from(this.encryptionKey, 'hex') + if (key.length !== 32) { + throw new Error('Encryption key must be 64 hex characters (32 bytes)') + } + + const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES) + const message = JSON.stringify(note) + + const ciphertext = sodium.crypto_secretbox_easy( + Buffer.from(message), + nonce, + key + ) + + const combined = Buffer.concat([nonce, ciphertext]) + return combined.toString('base64') + } + + _decrypt(encryptedData) { + if (!this.encryptionKey) { + throw new Error('Encryption key not set') + } + + const key = Buffer.from(this.encryptionKey, 'hex') + if (key.length !== 32) { + throw new Error('Encryption key must be 64 hex characters (32 bytes)') + } + + const combined = Buffer.from(encryptedData, 'base64') + const nonce = combined.slice(0, sodium.crypto_secretbox_NONCEBYTES) + const ciphertext = combined.slice(sodium.crypto_secretbox_NONCEBYTES) + + const decrypted = sodium.crypto_secretbox_open_easy( + ciphertext, + nonce, + key + ) + + return JSON.parse(decrypted.toString()) + } + async init() { + await this._initSodium() await this.adapter.init() - const notes = await this.adapter.getAll() + const encryptedNotes = await this.adapter.getAll() - for (const note of notes) { - this.notesCache.set(note.id, note) - this.index.add(note.id, note.title + '\n' + note.content) + for (const encryptedNote of encryptedNotes) { + try { + const note = this._decrypt(encryptedNote.data || encryptedNote) + this.notesCache.set(note.id, note) + this.index.add(note.id, note.title + '\n' + note.content) + } catch (error) { + console.error('Failed to decrypt note:', error) + } } } @@ -65,10 +127,15 @@ export default class NotesAPI { content, } + const encryptedNote = { + id: note.id, + data: this._encrypt(note), + } + this.notesCache.set(id, note) this.index.add(id, note.title + '\n' + content) - await this.adapter.create(note) + await this.adapter.create(encryptedNote) return note } @@ -87,9 +154,14 @@ export default class NotesAPI { note.content = content note.updatedAt = new Date().toISOString() + const encryptedNote = { + id: note.id, + data: this._encrypt(note), + } + this.index.update(id, note.title + '\n' + content) - await this.adapter.update(note) + await this.adapter.update(encryptedNote) return note } @@ -115,9 +187,14 @@ export default class NotesAPI { note.updatedAt = new Date().toISOString() + const encryptedNote = { + id: note.id, + data: this._encrypt(note), + } + this.index.update(id, note.title + '\n' + note.content) - await this.adapter.update(note) + await this.adapter.update(encryptedNote) return note } diff --git a/src/main/index.js b/src/main/index.js index e47ecc0..200e606 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -82,16 +82,19 @@ app.whenReady().then(async () => { const config = await new PluginConfig(filesystemPlugin).load() // Create instance of active adapter - // const plugin = registry.get(config.activeAdapter) - const plugin = registry.get(supabasePlugin.id) - // const adapter = plugin.createAdapter(config.adapterConfig) - const adapter = plugin.createAdapter({ - supabaseKey: process.env.SUPABASE_KEY, - supabaseUrl: process.env.SUPABASE_URL, - }) + const plugin = registry.get(config.activeAdapter) + // const plugin = registry.get(supabasePlugin.id) + const adapter = plugin.createAdapter(config.adapterConfig) + // const adapter = plugin.createAdapter({ + // supabaseKey: process.env.SUPABASE_KEY, + // supabaseUrl: process.env.SUPABASE_URL, + // }) // Init Notes API - const notesAPI = new NotesAPI(adapter) + const notesAPI = new NotesAPI( + adapter, + '729a0d21d783654c68f1a0123e2a0e986350de536b5324f1f35876ea12ffeaf5', + ) await notesAPI.init() // Handle Notes API