diff --git a/backend/app/__pycache__/__init__.cpython-314.pyc b/backend/app/__pycache__/__init__.cpython-314.pyc index e78c6d7..5d59c99 100644 Binary files a/backend/app/__pycache__/__init__.cpython-314.pyc and b/backend/app/__pycache__/__init__.cpython-314.pyc differ diff --git a/backend/app/__pycache__/database.cpython-314.pyc b/backend/app/__pycache__/database.cpython-314.pyc index c394bc6..ddda32d 100644 Binary files a/backend/app/__pycache__/database.cpython-314.pyc and b/backend/app/__pycache__/database.cpython-314.pyc differ diff --git a/backend/app/__pycache__/main.cpython-314.pyc b/backend/app/__pycache__/main.cpython-314.pyc index 1c5498b..a0a2cf3 100644 Binary files a/backend/app/__pycache__/main.cpython-314.pyc and b/backend/app/__pycache__/main.cpython-314.pyc differ diff --git a/backend/app/__pycache__/models.cpython-314.pyc b/backend/app/__pycache__/models.cpython-314.pyc index 001efb1..25f616e 100644 Binary files a/backend/app/__pycache__/models.cpython-314.pyc and b/backend/app/__pycache__/models.cpython-314.pyc differ diff --git a/backend/app/routes/__pycache__/folders.cpython-314.pyc b/backend/app/routes/__pycache__/folders.cpython-314.pyc index 892c122..e108813 100644 Binary files a/backend/app/routes/__pycache__/folders.cpython-314.pyc and b/backend/app/routes/__pycache__/folders.cpython-314.pyc differ diff --git a/backend/app/routes/__pycache__/notes.cpython-314.pyc b/backend/app/routes/__pycache__/notes.cpython-314.pyc index adec199..4c1f6ad 100644 Binary files a/backend/app/routes/__pycache__/notes.cpython-314.pyc and b/backend/app/routes/__pycache__/notes.cpython-314.pyc differ diff --git a/backend/notes.db b/backend/notes.db index a0dfa56..c5e77e4 100644 Binary files a/backend/notes.db and b/backend/notes.db differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index cf96f99..260d801 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -59,7 +59,6 @@ "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -636,7 +635,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.11.3.tgz", "integrity": "sha512-9HBM2XnwDj7fnu0551HkGdrUrrqmYq/WC5iv6nbY2WdicXdGbhR/gfbZOH73Aqj4351alY1+aoG9rCNfiwS1RA==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.23.0", @@ -726,7 +724,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", "license": "MIT", - "peer": true, "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } @@ -736,7 +733,6 @@ "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.8.tgz", "integrity": "sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==", "license": "MIT", - "peer": true, "dependencies": { "@codemirror/state": "^6.5.0", "crelt": "^1.0.6", @@ -1273,7 +1269,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", "license": "MIT", - "peer": true, "dependencies": { "@fortawesome/fontawesome-common-types": "7.1.0" }, @@ -1638,7 +1633,6 @@ "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", "license": "MIT", - "peer": true, "dependencies": { "@lezer/common": "^1.3.0" } @@ -3336,7 +3330,6 @@ "integrity": "sha512-p/jUvulfgU7oKtj6Xpk8cA2Y1xKTtICGpJYeJXz2YVO2UcvjQgeRMLDGfDeqeRW2Ta+0QNFwcc8X3GH8SxZz6w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3347,7 +3340,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3384,7 +3376,6 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3492,7 +3483,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.25", "caniuse-lite": "^1.0.30001754", @@ -4351,6 +4341,7 @@ "resolved": "https://registry.npmjs.org/isomorphic.js/-/isomorphic.js-0.2.5.tgz", "integrity": "sha512-PIeMbHqMt4DnUP3MA/Flc0HElYjMXArsw1qwJZcm9sqR8mq3l8NYizFMty0pWwE/tzIGH3EKK5+jes5mAr85yw==", "license": "MIT", + "peer": true, "funding": { "type": "GitHub Sponsors ❤", "url": "https://github.com/sponsors/dmonad" @@ -4429,6 +4420,7 @@ "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.114.tgz", "integrity": "sha512-gcxmNFzA4hv8UYi8j43uPlQ7CGcyMJ2KQb5kZASw6SnAKAf10hK12i2fjrS3Cl/ugZa5Ui6WwIu1/6MIXiHttQ==", "license": "MIT", + "peer": true, "dependencies": { "isomorphic.js": "^0.2.4" }, @@ -5896,7 +5888,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -5918,7 +5909,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6217,8 +6207,7 @@ "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -6439,7 +6428,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", diff --git a/frontend/src/api/encryption.tsx b/frontend/src/api/encryption.tsx new file mode 100644 index 0000000..9e826cd --- /dev/null +++ b/frontend/src/api/encryption.tsx @@ -0,0 +1,95 @@ +import { FolderTreeResponse, FolderTreeNode } from "./folders"; + +export async function deriveKey(password: string) { + const enc = new TextEncoder(); + const keyMaterial = await crypto.subtle.importKey( + "raw", + enc.encode(password), + "PBKDF2", + false, + ["deriveKey"], + ); + + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: enc.encode("your-app-salt"), // Store this somewhere consistent + iterations: 100000, + hash: "SHA-256", + }, + keyMaterial, + { name: "AES-GCM", length: 256 }, + false, + ["encrypt", "decrypt"], + ); +} + +export async function encryptString( + text: string, + key: CryptoKey, +): Promise { + const enc = new TextEncoder(); + const iv = crypto.getRandomValues(new Uint8Array(12)); + + const encrypted = await crypto.subtle.encrypt( + { name: "AES-GCM", iv }, + key, + enc.encode(text), + ); + + const combined = new Uint8Array(iv.length + encrypted.byteLength); + combined.set(iv); + combined.set(new Uint8Array(encrypted), iv.length); + + return btoa(String.fromCharCode(...combined)); +} + +export async function decryptString(encrypted: string, key: CryptoKey) { + const combined = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0)); + const iv = combined.slice(0, 12); + const data = combined.slice(12); + + const decrypted = await crypto.subtle.decrypt( + { name: "AES-GCM", iv }, + key, + data, + ); + + return new TextDecoder().decode(decrypted); +} + +export async function decryptFolderTree( + tree: FolderTreeResponse, + encryptionKey: CryptoKey, +): Promise { + const decryptFolder = async ( + folder: FolderTreeNode, + ): Promise => { + return { + ...folder, + notes: await Promise.all( + folder.notes.map(async (note) => ({ + ...note, + title: await decryptString(note.title, encryptionKey), + content: await decryptString(note.content, encryptionKey), + })), + ), + children: await Promise.all( + folder.children.map((child) => decryptFolder(child)), + ), + }; + }; + + return { + folders: await Promise.all( + tree.folders.map((folder) => decryptFolder(folder)), + ), + orphaned_notes: await Promise.all( + tree.orphaned_notes.map(async (note) => ({ + ...note, + title: await decryptString(note.title, encryptionKey), + content: await decryptString(note.content, encryptionKey), + })), + ), + }; +} diff --git a/frontend/src/api/folders.tsx b/frontend/src/api/folders.tsx index 8ca5409..6e5ab22 100644 --- a/frontend/src/api/folders.tsx +++ b/frontend/src/api/folders.tsx @@ -1,4 +1,5 @@ import axios from "axios"; +import { decryptFolderTree, deriveKey } from "./encryption"; const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/api"; @@ -35,8 +36,18 @@ export interface FolderCreate { parent_id: number | null; } +const getFolderTree = async () => { + const { data } = await axios.get( + `${API_URL}/folders/tree`, + ); + var key = await deriveKey("Test"); + const decryptedFolderTree = await decryptFolderTree(data, key); + + return decryptedFolderTree; +}; + export const folderApi = { - tree: () => axios.get(`${API_URL}/folders/tree`), + tree: () => getFolderTree(), list: () => axios.get(`${API_URL}/folders`), create: (folder: FolderCreate) => axios.post(`${API_URL}/folders`, folder), diff --git a/frontend/src/api/notes.tsx b/frontend/src/api/notes.tsx index 9bb4b5d..8e69c89 100644 --- a/frontend/src/api/notes.tsx +++ b/frontend/src/api/notes.tsx @@ -1,4 +1,6 @@ import axios from "axios"; +import { NoteRead } from "./folders"; +import { deriveKey, encryptString, decryptString } from "./encryption"; const API_URL = import.meta.env.PROD ? "/api" : "http://localhost:8000/api"; @@ -18,82 +20,39 @@ export interface NoteCreate { encrypted: boolean; } -// Derive key from password -async function deriveKey(password: string) { - const enc = new TextEncoder(); - const keyMaterial = await crypto.subtle.importKey( - "raw", - enc.encode(password), - "PBKDF2", - false, - ["deriveKey"], - ); - - return crypto.subtle.deriveKey( - { - name: "PBKDF2", - salt: enc.encode("your-app-salt"), // Store this somewhere consistent - iterations: 100000, - hash: "SHA-256", - }, - keyMaterial, - { name: "AES-GCM", length: 256 }, - false, - ["encrypt", "decrypt"], - ); -} - -// Encrypt content -async function encryptNote(content: string, key: CryptoKey) { - const enc = new TextEncoder(); - const iv = crypto.getRandomValues(new Uint8Array(12)); - - const encrypted = await crypto.subtle.encrypt( - { name: "AES-GCM", iv }, - key, - enc.encode(content), - ); - - // Return IV + encrypted data as base64 - const combined = new Uint8Array(iv.length + encrypted.byteLength); - combined.set(iv); - combined.set(new Uint8Array(encrypted), iv.length); - - return btoa(String.fromCharCode(...combined)); -} - -// Decrypt content -async function decryptNote(encrypted: string, key: CryptoKey) { - const combined = Uint8Array.from(atob(encrypted), (c) => c.charCodeAt(0)); - const iv = combined.slice(0, 12); - const data = combined.slice(12); - - const decrypted = await crypto.subtle.decrypt( - { name: "AES-GCM", iv }, - key, - data, - ); - - return new TextDecoder().decode(decrypted); -} - const createNote = async (note: NoteCreate) => { - if (!note.encrypted) { - return axios.post(`${API_URL}/notes`, note); - } else { - var key = await deriveKey("Test"); - var eNote = await encryptNote(note.content, key); + var key = await deriveKey("Test"); + var noteContent = await encryptString(note.content, key); + var noteTitle = await encryptString(note.title, key); - console.log(eNote); + var encryptedNote = { + title: noteTitle, + content: noteContent, + folder_id: note.folder_id, + }; - var unENote = await decryptNote(eNote, key); + console.log(encryptedNote); + return axios.post(`${API_URL}/notes`, encryptedNote); +}; - console.log(unENote); - } +const fetchNotes = async () => { + const { data } = await axios.get(`${API_URL}/notes`); + + console.log(data); + var key = await deriveKey("Test"); + const decryptedNotes = await Promise.all( + data.map(async (note: Note) => ({ + ...note, + title: await decryptString(note.title, key), + content: await decryptString(note.content, key), + })), + ); + + return decryptedNotes; }; export const notesApi = { - list: () => axios.get(`${API_URL}/notes`), + list: () => fetchNotes(), get: (id: number) => axios.get(`${API_URL}/notes/${id}`), create: (note: NoteCreate) => createNote(note), update: (id: number, note: Partial) => diff --git a/frontend/src/components/sidebar/DraggableNote.tsx b/frontend/src/components/sidebar/DraggableNote.tsx index 847c313..0bf33d3 100644 --- a/frontend/src/components/sidebar/DraggableNote.tsx +++ b/frontend/src/components/sidebar/DraggableNote.tsx @@ -27,7 +27,7 @@ export const DraggableNote = ({
selectNote(note)} - className={`ml-5 rounded-md px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${ + className={` rounded-md px-2 mb-0.5 select-none cursor-pointer font-light transition-all duration-150 flex items-center gap-1 ${ selectedNote?.id === note.id ? "bg-ctp-mauve text-ctp-base" : "hover:bg-ctp-surface1" diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 307fa2e..f01076b 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -68,7 +68,7 @@ function Home() { }, []); const loadFolderTree = async () => { - const { data } = await folderApi.tree(); + const data = await folderApi.tree(); setFolderTree(data); }; @@ -113,7 +113,7 @@ function Home() { }; const selectNote = (note: NoteRead) => { - console.log("setting note.... " + note.id); + console.log(note); setSelectedNote(note); setTitle(note.title); setContent(note.content); @@ -146,14 +146,16 @@ function Home() { selectedFolder={selectedFolder} selectedNote={selectedNote} /> - {folder.notes.map((note) => ( - - ))} +
+ {folder.notes.map((note) => ( + + ))} +
{folder.children.map((child) => renderFolder(child, depth + 1))}
); @@ -176,14 +178,9 @@ function Home() {
e.preventDefault()} // Add this - onTouchMove={(e) => e.preventDefault()} // And this for touch devices + className="bg-ctp-mantle border-r-ctp-surface2 border-r overflow-hidden w-[300px] p-4 overflow-y-auto sm:block hidden" + onDragOver={(e) => e.preventDefault()} + onTouchMove={(e) => e.preventDefault()} >

Notes