From b05841a562bbda16c5e3619d26b2f37eeb7f8af1 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Tue, 23 May 2023 09:37:33 +0200 Subject: [PATCH 01/17] add timeout --- packages/proxy/proxy.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/proxy/proxy.js b/packages/proxy/proxy.js index 0173cc4..7e5292f 100644 --- a/packages/proxy/proxy.js +++ b/packages/proxy/proxy.js @@ -10,7 +10,7 @@ proxy.on('error', function (err, req, res) { const server = http.createServer(function (req, res) { const target = req.url.startsWith('/api/') ? 'http://127.0.0.1:8000' : 'http://localhost:8001' - proxy.web(req, res, { target }) + proxy.web(req, res, { target, proxyTimeout: 250, timeout: 250 }) }) server.listen(1234) console.log('Proxy on http://localhost:1234') From c2bfe9dd0dee9b4cce5d780852be3d928eef0daf Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Tue, 23 May 2023 09:37:49 +0200 Subject: [PATCH 02/17] add derivation to metadata --- packages/shared/src/api.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/packages/shared/src/api.ts b/packages/shared/src/api.ts index fe65566..e0257a8 100644 --- a/packages/shared/src/api.ts +++ b/packages/shared/src/api.ts @@ -1,6 +1,9 @@ -import type { TypedArray } from 'occulto' +import type { KeyData, TypedArray } from 'occulto' -export type NoteMeta = { type: 'text' | 'file' } +export type NoteMeta = { + type: 'text' | 'file' + derivation?: KeyData +} export type Note = { contents: string @@ -8,7 +11,7 @@ export type Note = { views?: number expiration?: number } -export type NoteInfo = {} +export type NoteInfo = Pick export type NotePublic = Pick export type NoteCreate = Omit & { meta: string } @@ -71,10 +74,12 @@ export async function get(id: string): Promise { method: 'delete', }) const { contents, meta } = data - return { + const note = { contents, - meta: JSON.parse(meta) as NoteMeta, - } + meta: JSON.parse(meta), + } satisfies NotePublic + if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt)) + return note } export async function info(id: string): Promise { @@ -82,7 +87,12 @@ export async function info(id: string): Promise { url: `notes/${id}`, method: 'get', }) - return data + const { meta } = data + const note = { + meta: JSON.parse(meta), + } satisfies NoteInfo + if (note.meta.derivation) note.meta.derivation.salt = new Uint8Array(Object.values(note.meta.derivation.salt)) + return note } export type Status = { From 85204776d754fe55b3a1156a51358a28627e97df Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Tue, 23 May 2023 09:38:00 +0200 Subject: [PATCH 03/17] demo postman collection --- Cryptgeon.postman_collection.json | 593 ++++++++++++++++++++++++++++++ 1 file changed, 593 insertions(+) create mode 100644 Cryptgeon.postman_collection.json diff --git a/Cryptgeon.postman_collection.json b/Cryptgeon.postman_collection.json new file mode 100644 index 0000000..7919ae9 --- /dev/null +++ b/Cryptgeon.postman_collection.json @@ -0,0 +1,593 @@ +{ + "info": { + "_postman_id": "52d9e661-2d99-47f8-b09a-40b6a1c0b364", + "name": "Cryptgeon", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "item": [ + { + "name": "Notes", + "item": [ + { + "name": "Preview", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE}}/notes/:id", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{NOTE_ID}}", + "description": "Id of the Note" + } + ] + }, + "description": "This endpoint is to query wether a note exists, without actually opening it. No view limits are used here, as contents of the note are not available, only the `meta` field is returned, which is public." + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE}}/notes/:id", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{NOTE_ID}}", + "description": "Id of the Note" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "transfer-encoding", + "value": "chunked" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "content-encoding", + "value": "gzip" + }, + { + "key": "vary", + "value": "accept-encoding" + }, + { + "key": "date", + "value": "Tue, 23 May 2023 05:24:29 GMT" + } + ], + "cookie": [], + "body": "{}" + }, + { + "name": "404", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE}}/notes/:id", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{NOTE_ID}}", + "description": "Id of the Note" + } + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "plain", + "header": [ + { + "key": "transfer-encoding", + "value": "chunked" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "vary", + "value": "accept-encoding" + }, + { + "key": "content-encoding", + "value": "gzip" + }, + { + "key": "date", + "value": "Tue, 23 May 2023 05:25:26 GMT" + } + ], + "cookie": [], + "body": null + } + ] + }, + { + "name": "Create", + "event": [ + { + "listen": "test", + "script": { + "exec": [ + "const jsonData = pm.response.json();", + "pm.collectionVariables.set('NOTE_ID', jsonData.id)", + "" + ], + "type": "text/javascript" + } + } + ], + "request": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 1,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE}}/notes/", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + "" + ] + } + }, + "response": [ + { + "name": "Simple", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 1,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE}}/notes/", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + "" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "transfer-encoding", + "value": "chunked" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-encoding", + "value": "gzip" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "vary", + "value": "accept-encoding" + }, + { + "key": "date", + "value": "Tue, 23 May 2023 05:31:54 GMT" + } + ], + "cookie": [], + "body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}" + }, + { + "name": "5 Minutes", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"contents\": \"Some encrypted content\",\n \"expiration\": 5,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE}}/notes/", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + "" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "transfer-encoding", + "value": "chunked" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-encoding", + "value": "gzip" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "vary", + "value": "accept-encoding" + }, + { + "key": "date", + "value": "Tue, 23 May 2023 05:31:54 GMT" + } + ], + "cookie": [], + "body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}" + }, + { + "name": "3 Views", + "originalRequest": { + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"contents\": \"Some encrypted content\",\n \"views\": 3,\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\"\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "{{BASE}}/notes/", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + "" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "transfer-encoding", + "value": "chunked" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-encoding", + "value": "gzip" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "vary", + "value": "accept-encoding" + }, + { + "key": "date", + "value": "Tue, 23 May 2023 05:31:54 GMT" + } + ], + "cookie": [], + "body": "{\n \"id\": \"1QeEWDQbQY9dOo8cDDQjykaEjouqugTR6A78sjgn4VMv\"\n}" + } + ] + }, + { + "name": "Read", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{BASE}}/notes/:id", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{NOTE_ID}}" + } + ] + }, + "description": "This endpoint gets the actual contents of a note. It's a `DELETE` endpoint, es it decreases the `view` counter, and deletes the note if `0` is reached." + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{BASE}}/notes/:id", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{NOTE_ID}}" + } + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "transfer-encoding", + "value": "chunked" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "vary", + "value": "accept-encoding" + }, + { + "key": "content-encoding", + "value": "gzip" + }, + { + "key": "date", + "value": "Tue, 23 May 2023 05:59:07 GMT" + } + ], + "cookie": [], + "body": "{\n \"meta\": \"{\\\"type\\\":\\\"text\\\"}\",\n \"contents\": \"Some encrypted content\"\n}" + }, + { + "name": "404", + "originalRequest": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{BASE}}/notes/:id", + "host": [ + "{{BASE}}" + ], + "path": [ + "notes", + ":id" + ], + "variable": [ + { + "key": "id", + "value": "{{NOTE_ID}}" + } + ] + } + }, + "status": "Not Found", + "code": 404, + "_postman_previewlanguage": "plain", + "header": [ + { + "key": "transfer-encoding", + "value": "chunked" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "vary", + "value": "accept-encoding" + }, + { + "key": "content-encoding", + "value": "gzip" + }, + { + "key": "date", + "value": "Tue, 23 May 2023 05:59:15 GMT" + } + ], + "cookie": [], + "body": null + } + ] + } + ] + }, + { + "name": "Status", + "item": [ + { + "name": "Get", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE}}/status/", + "host": [ + "{{BASE}}" + ], + "path": [ + "status", + "" + ] + } + }, + "response": [ + { + "name": "200", + "originalRequest": { + "method": "GET", + "header": [], + "url": { + "raw": "{{BASE}}/status/", + "host": [ + "{{BASE}}" + ], + "path": [ + "status", + "" + ] + } + }, + "status": "OK", + "code": 200, + "_postman_previewlanguage": "json", + "header": [ + { + "key": "transfer-encoding", + "value": "chunked" + }, + { + "key": "connection", + "value": "close" + }, + { + "key": "content-encoding", + "value": "gzip" + }, + { + "key": "vary", + "value": "accept-encoding" + }, + { + "key": "content-type", + "value": "application/json" + }, + { + "key": "date", + "value": "Tue, 23 May 2023 05:56:45 GMT" + } + ], + "cookie": [], + "body": "{\n \"version\": \"2.3.0-beta.4\",\n \"max_size\": 10485760,\n \"max_views\": 100,\n \"max_expiration\": 360,\n \"allow_advanced\": true,\n \"theme_image\": \"\",\n \"theme_text\": \"\",\n \"theme_page_title\": \"\",\n \"theme_favicon\": \"\"\n}" + } + ] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ], + "variable": [ + { + "key": "BASE", + "value": "http://localhost:1234/api", + "type": "default" + }, + { + "key": "NOTE_ID", + "value": "", + "type": "default" + } + ] +} \ No newline at end of file From 6000553b954b1a1985fccdcd43b5c502bc81f118 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Tue, 23 May 2023 09:38:23 +0200 Subject: [PATCH 04/17] include meta in info endpoint --- packages/backend/src/note/model.rs | 4 +++- packages/backend/src/note/routes.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/backend/src/note/model.rs b/packages/backend/src/note/model.rs index 8dfdfaf..36a2618 100644 --- a/packages/backend/src/note/model.rs +++ b/packages/backend/src/note/model.rs @@ -11,7 +11,9 @@ pub struct Note { } #[derive(Serialize, Deserialize, Clone)] -pub struct NoteInfo {} +pub struct NoteInfo { + pub meta: String, +} #[derive(Serialize, Deserialize, Clone)] pub struct NotePublic { diff --git a/packages/backend/src/note/routes.rs b/packages/backend/src/note/routes.rs index d34ad8d..b6a9c10 100644 --- a/packages/backend/src/note/routes.rs +++ b/packages/backend/src/note/routes.rs @@ -24,7 +24,7 @@ async fn one(path: web::Path) -> impl Responder { let note = store::get(&p.id); match note { - Ok(Some(_)) => HttpResponse::Ok().json(NoteInfo {}), + Ok(Some(n)) => HttpResponse::Ok().json(NoteInfo { meta: n.meta }), Ok(None) => HttpResponse::NotFound().finish(), Err(e) => HttpResponse::InternalServerError().body(e.to_string()), } From e6d1e0f44ae5665377f986bac1ae8f0c6aed2689 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Tue, 23 May 2023 09:39:00 +0200 Subject: [PATCH 05/17] add password to CLI --- packages/cli/src/download.ts | 25 ++++++++++--- packages/cli/src/index.ts | 11 ++++-- packages/cli/src/stdin.ts | 20 +++++++++++ packages/cli/src/upload.ts | 69 +++++++++++++++++++----------------- 4 files changed, 84 insertions(+), 41 deletions(-) create mode 100644 packages/cli/src/stdin.ts diff --git a/packages/cli/src/download.ts b/packages/cli/src/download.ts index 4da04ef..88a72d1 100644 --- a/packages/cli/src/download.ts +++ b/packages/cli/src/download.ts @@ -2,7 +2,7 @@ import { Adapters, get, info, setBase } from '@cryptgeon/shared' import inquirer from 'inquirer' import { access, constants, writeFile } from 'node:fs/promises' import { basename, resolve } from 'node:path' -import { Hex } from 'occulto' +import { AES, Hex } from 'occulto' import pretty from 'pretty-bytes' import { exit } from './utils' @@ -10,11 +10,26 @@ import { exit } from './utils' export async function download(url: URL) { setBase(url.origin) const id = url.pathname.split('/')[2] - await info(id).catch(() => exit('Note does not exist or is expired')) - const note = await get(id) + const preview = await info(id).catch(() => exit('Note does not exist or is expired')) - const password = url.hash.slice(1) - const key = Hex.decode(password) + // Password + let password: string + const derivation = preview?.meta.derivation + if (derivation) { + const response = await inquirer.prompt([ + { + type: 'password', + message: 'Note password', + name: 'password', + }, + ]) + password = response.password + } else { + password = url.hash.slice(1) + } + + const key = derivation ? (await AES.derive(password, derivation))[0] : Hex.decode(password) + const note = await get(id) const couldNotDecrypt = () => exit('Could not decrypt note. Probably an invalid password') switch (note.meta.type) { diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 19356fe..af86cfe 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -6,7 +6,8 @@ import prettyBytes from 'pretty-bytes' import { download } from './download.js' import { parseFile, parseNumber } from './parsers.js' -import { uploadFiles, uploadText } from './upload.js' +import { getStdin } from './stdin.js' +import { upload } from './upload.js' import { exit } from './utils.js' const defaultServer = process.env['CRYPTGEON_SERVER'] || 'https://cryptgeon.org' @@ -61,10 +62,12 @@ send .addOption(server) .addOption(views) .addOption(minutes) + .addOption(password) .action(async (files, options) => { setBase(options.server!) await checkConstrains(options) - await uploadFiles(files, { views: options.views, expiration: options.minutes }) + options.password ||= await getStdin() + await upload(files, { views: options.views, expiration: options.minutes, password: options.password }) }) send .command('text') @@ -72,10 +75,12 @@ send .addOption(server) .addOption(views) .addOption(minutes) + .addOption(password) .action(async (text, options) => { setBase(options.server!) await checkConstrains(options) - await uploadText(text, { views: options.views, expiration: options.minutes }) + options.password ||= await getStdin() + await upload(text, { views: options.views, expiration: options.minutes, password: options.password }) }) program diff --git a/packages/cli/src/stdin.ts b/packages/cli/src/stdin.ts new file mode 100644 index 0000000..d7714e6 --- /dev/null +++ b/packages/cli/src/stdin.ts @@ -0,0 +1,20 @@ +export function getStdin(timeout: number = 10): Promise { + return new Promise((resolve, reject) => { + // Store the data from stdin in a buffer + let buffer = '' + process.stdin.on('data', (d) => (buffer += d.toString())) + + // Stop listening for data after the timeout, otherwise hangs indefinitely + const t = setTimeout(() => { + process.stdin.destroy() + resolve('') + }, timeout) + + // Listen for end and error events + process.stdin.on('end', () => { + clearTimeout(t) + resolve(buffer.trim()) + }) + process.stdin.on('error', reject) + }) +} diff --git a/packages/cli/src/upload.ts b/packages/cli/src/upload.ts index ff7e6c1..9f4a7e2 100644 --- a/packages/cli/src/upload.ts +++ b/packages/cli/src/upload.ts @@ -1,48 +1,51 @@ import { readFile, stat } from 'node:fs/promises' import { basename } from 'node:path' -import { Adapters, BASE, create, FileDTO, Note } from '@cryptgeon/shared' +import { Adapters, BASE, create, FileDTO, Note, NoteMeta } from '@cryptgeon/shared' import mime from 'mime' import { AES, Hex, TypedArray } from 'occulto' import { exit } from './utils.js' -type UploadOptions = Pick +type UploadOptions = Pick & { password?: string } -export async function upload(key: TypedArray, note: Note) { +export async function upload(input: string | string[], options: UploadOptions) { try { + const { password, ...noteOptions } = options + const derived = options.password ? await AES.derive(options.password) : undefined + const key = derived ? derived[0] : await AES.generateKey() + + let contents: string + let type: NoteMeta['type'] + if (typeof input === 'string') { + contents = await Adapters.Text.encrypt(input, key) + type = 'text' + } else { + const files: FileDTO[] = await Promise.all( + input.map(async (path) => { + const data = new Uint8Array(await readFile(path)) + const stats = await stat(path) + const extension = path.substring(path.indexOf('.') + 1) + const type = mime.getType(extension) ?? 'application/octet-stream' + return { + name: basename(path), + size: stats.size, + contents: data, + type, + } satisfies FileDTO + }) + ) + contents = await Adapters.Files.encrypt(files, key) + type = 'file' + } + + // Create the actual note and upload it. + const note: Note = { ...noteOptions, contents, meta: { type, derivation: derived?.[1] } } const result = await create(note) - const password = Hex.encode(key) - const url = `${BASE}/note/${result.id}#${password}` - console.log(`Note created under:\n\n${url}`) + let url = `${BASE}/note/${result.id}` + if (!derived) url += `#${Hex.encode(key)}` + console.log(`Note created:\n\n${url}`) } catch { exit('Could not create note') } } - -export async function uploadFiles(paths: string[], options: UploadOptions) { - const key = await AES.generateKey() - const files: FileDTO[] = await Promise.all( - paths.map(async (path) => { - const data = new Uint8Array(await readFile(path)) - const stats = await stat(path) - const extension = path.substring(path.indexOf('.') + 1) - const type = mime.getType(extension) ?? 'application/octet-stream' - return { - name: basename(path), - size: stats.size, - contents: data, - type, - } satisfies FileDTO - }) - ) - - const contents = await Adapters.Files.encrypt(files, key) - await upload(key, { ...options, contents, meta: { type: 'file' } }) -} - -export async function uploadText(text: string, options: UploadOptions) { - const key = await AES.generateKey() - const contents = await Adapters.Text.encrypt(text, key) - await upload(key, { ...options, contents, meta: { type: 'text' } }) -} From 6da28a701e23cb11de1e43eb54b8f1843a32785c Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Tue, 23 May 2023 09:39:06 +0200 Subject: [PATCH 06/17] translations --- packages/frontend/locales/de.json | 7 ++++++- packages/frontend/locales/en.json | 7 ++++++- packages/frontend/locales/es.json | 7 ++++++- packages/frontend/locales/fr.json | 7 ++++++- packages/frontend/locales/it.json | 7 ++++++- packages/frontend/locales/ja.json | 7 ++++++- packages/frontend/locales/ru.json | 7 ++++++- packages/frontend/locales/zh.json | 7 ++++++- 8 files changed, 48 insertions(+), 8 deletions(-) diff --git a/packages/frontend/locales/de.json b/packages/frontend/locales/de.json index e2a926b..8afe4fc 100644 --- a/packages/frontend/locales/de.json +++ b/packages/frontend/locales/de.json @@ -16,7 +16,8 @@ "decrypting": "entschlüsselt", "uploading": "hochladen", "downloading": "wird heruntergeladen", - "qr_code": "qr-code" + "qr_code": "qr-code", + "password": "Passwort" }, "home": { "intro": "Senden Sie ganz einfach vollständig verschlüsselte, sichere Notizen oder Dateien mit einem Klick. Erstellen Sie einfach eine Notiz und teilen Sie den Link.", @@ -31,6 +32,10 @@ }, "messages": { "note_created": "notiz erstellt." + }, + "advanced": { + "explanation": "Standardmäßig wird für jede Notiz ein sicher generiertes Passwort verwendet. Sie können jedoch auch ein eigenes Kennwort wählen, das nicht in dem Link enthalten ist.", + "custom_password": "benutzerdefiniertes Passwort" } }, "show": { diff --git a/packages/frontend/locales/en.json b/packages/frontend/locales/en.json index 0b648d3..cd241e9 100644 --- a/packages/frontend/locales/en.json +++ b/packages/frontend/locales/en.json @@ -16,7 +16,8 @@ "decrypting": "decrypting", "uploading": "uploading", "downloading": "downloading", - "qr_code": "qr code" + "qr_code": "qr code", + "password": "password" }, "home": { "intro": "Easily send fully encrypted, secure notes or files with one click. Just create a note and share the link.", @@ -31,6 +32,10 @@ }, "messages": { "note_created": "note created." + }, + "advanced": { + "explanation": "By default, a securely generated password is used for each note. You can however also choose your own password, which is not included in the link.", + "custom_password": "custom password" } }, "show": { diff --git a/packages/frontend/locales/es.json b/packages/frontend/locales/es.json index 8890e37..5bb90c4 100644 --- a/packages/frontend/locales/es.json +++ b/packages/frontend/locales/es.json @@ -16,7 +16,8 @@ "decrypting": "descifrando", "uploading": "cargando", "downloading": "descargando", - "qr_code": "código qr" + "qr_code": "código qr", + "password": "contraseña" }, "home": { "intro": "Envía fácilmente notas o archivos totalmente encriptados y seguros con un solo clic. Solo tienes que crear una nota y compartir el enlace.", @@ -31,6 +32,10 @@ }, "messages": { "note_created": "nota creada." + }, + "advanced": { + "explanation": "Por defecto, se utiliza una contraseña generada de forma segura para cada nota. No obstante, también puede elegir su propia contraseña, que no se incluye en el enlace.", + "custom_password": "contraseña personalizada" } }, "show": { diff --git a/packages/frontend/locales/fr.json b/packages/frontend/locales/fr.json index 36d8bf6..90bd345 100644 --- a/packages/frontend/locales/fr.json +++ b/packages/frontend/locales/fr.json @@ -16,7 +16,8 @@ "decrypting": "déchiffrer", "uploading": "téléchargement", "downloading": "téléchargement", - "qr_code": "code qr" + "qr_code": "code qr", + "password": "mot de passe" }, "home": { "intro": "Envoyez facilement des notes ou des fichiers entièrement cryptés et sécurisés en un seul clic. Il suffit de créer une note et de partager le lien.", @@ -31,6 +32,10 @@ }, "messages": { "note_created": "note créée." + }, + "advanced": { + "explanation": "Par défaut, un mot de passe généré de manière sécurisée est utilisé pour chaque note. Vous pouvez toutefois choisir votre propre mot de passe, qui n'est pas inclus dans le lien.", + "custom_password": "mot de passe personnalisé" } }, "show": { diff --git a/packages/frontend/locales/it.json b/packages/frontend/locales/it.json index f1146cf..a31a302 100644 --- a/packages/frontend/locales/it.json +++ b/packages/frontend/locales/it.json @@ -16,7 +16,8 @@ "decrypting": "decifrando", "uploading": "caricamento", "downloading": "scaricando", - "qr_code": "codice qr" + "qr_code": "codice qr", + "password": "password" }, "home": { "intro": "Invia facilmente note o file completamente criptati e sicuri con un solo clic. Basta creare una nota e condividere il link.", @@ -31,6 +32,10 @@ }, "messages": { "note_created": "nota creata." + }, + "advanced": { + "explanation": "Per impostazione predefinita, per ogni nota viene utilizzata una password generata in modo sicuro. È tuttavia possibile scegliere la propria password, che non è inclusa nel link.", + "custom_password": "password personalizzata" } }, "show": { diff --git a/packages/frontend/locales/ja.json b/packages/frontend/locales/ja.json index 1f6c778..b971a09 100644 --- a/packages/frontend/locales/ja.json +++ b/packages/frontend/locales/ja.json @@ -16,7 +16,8 @@ "decrypting": "復号化", "uploading": "アップロード中", "downloading": "ダウンロード中", - "qr_code": "QRコード" + "qr_code": "QRコード", + "password": "暗号" }, "home": { "intro": "完全に暗号化された 、安全なメモやファイルをワンクリックで簡単に送信できます。メモを作成してリンクを共有するだけです。", @@ -31,6 +32,10 @@ }, "messages": { "note_created": "メモが作成されました。" + }, + "advanced": { + "explanation": "デフォルトでは、安全に生成されたパスワードが各ノートに使用されます。しかし、リンクに含まれない独自のパスワードを選択することもできます。", + "custom_password": "カスタムパスワード" } }, "show": { diff --git a/packages/frontend/locales/ru.json b/packages/frontend/locales/ru.json index beabd53..8406534 100644 --- a/packages/frontend/locales/ru.json +++ b/packages/frontend/locales/ru.json @@ -16,7 +16,8 @@ "decrypting": "расшифровка", "uploading": "загрузка", "downloading": "скачивание", - "qr_code": "qr код" + "qr_code": "qr код", + "password": "пароль" }, "home": { "intro": "Легко отправляйте полностью зашифрованные защищенные заметки или файлы одним щелчком мыши. Просто создайте заметку и поделитесь ссылкой.", @@ -31,6 +32,10 @@ }, "messages": { "note_created": "заметка создана." + }, + "advanced": { + "explanation": "По умолчанию для каждой заметки используется безопасно сгенерированный пароль. Однако вы также можете выбрать свой собственный пароль, который не включен в ссылку.", + "custom_password": "пользовательский пароль" } }, "show": { diff --git a/packages/frontend/locales/zh.json b/packages/frontend/locales/zh.json index a1fc664..2a78b10 100644 --- a/packages/frontend/locales/zh.json +++ b/packages/frontend/locales/zh.json @@ -16,7 +16,8 @@ "decrypting": "解密", "uploading": "上传", "downloading": "下载", - "qr_code": "二维码" + "qr_code": "二维码", + "password": "密码" }, "home": { "intro": "飞鸽传书,一键传输完全加密的密信或文件,阅后即焚。", @@ -31,6 +32,10 @@ }, "messages": { "note_created": "密信创建成功。" + }, + "advanced": { + "explanation": "默认情况下,每个笔记都使用安全生成的密码。但是,您也可以选择您自己的密码,该密码未包含在链接中。", + "custom_password": "自定义密码" } }, "show": { From fdc2722fb96cc85a902677d2d074af8377076ccd Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Tue, 23 May 2023 09:39:19 +0200 Subject: [PATCH 07/17] add password to frontend --- packages/frontend/src/app.css | 12 ++- .../src/lib/ui/AdvancedParameters.svelte | 79 ++++++++++++------- .../frontend/src/lib/ui/NoteResult.svelte | 5 +- packages/frontend/src/lib/ui/Switch.svelte | 42 ++++------ packages/frontend/src/lib/ui/TextInput.svelte | 3 +- packages/frontend/src/lib/views/Create.svelte | 11 +-- packages/frontend/src/lib/views/Footer.svelte | 1 + .../src/routes/note/[id]/+page.svelte | 41 ++++++++-- 8 files changed, 124 insertions(+), 70 deletions(-) diff --git a/packages/frontend/src/app.css b/packages/frontend/src/app.css index 2310eeb..97ff57f 100644 --- a/packages/frontend/src/app.css +++ b/packages/frontend/src/app.css @@ -92,7 +92,7 @@ button { } *:disabled, -*[disabled='true'] { +.disabled { opacity: 0.5; } @@ -126,3 +126,13 @@ fieldset { .tr { text-align: right; } + +hr { + border: none; + border-bottom: 2px solid var(--ui-bg-1); + margin: 1rem 0; +} + +p { + margin: 0; +} diff --git a/packages/frontend/src/lib/ui/AdvancedParameters.svelte b/packages/frontend/src/lib/ui/AdvancedParameters.svelte index 442e38a..88672e7 100644 --- a/packages/frontend/src/lib/ui/AdvancedParameters.svelte +++ b/packages/frontend/src/lib/ui/AdvancedParameters.svelte @@ -8,48 +8,69 @@ export let note: Note export let timeExpiration = false + + let customPassword = false + + $: if (!customPassword) note.password = undefined -
- - ($status && v <= $status?.max_views && v > 0) || - $t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })} - /> -
+
+
+ + ($status && v <= $status?.max_views && v > 0) || + $t('home.errors.max', { values: { n: $status?.max_views ?? 0 } })} + /> + + ($status && v < $status?.max_expiration) || + $t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })} + /> +
+
+ + +
+
+ {$t('home.advanced.explanation')}
- - ($status && v < $status?.max_expiration) || - $t('home.errors.max', { values: { n: $status?.max_expiration ?? 0 } })} - />
diff --git a/packages/frontend/src/lib/ui/NoteResult.svelte b/packages/frontend/src/lib/ui/NoteResult.svelte index 73884dc..8246e75 100644 --- a/packages/frontend/src/lib/ui/NoteResult.svelte +++ b/packages/frontend/src/lib/ui/NoteResult.svelte @@ -1,7 +1,7 @@ @@ -14,7 +14,8 @@ export let result: NoteResult - $: url = `${window.location.origin}/note/${result.id}#${result.password}` + let url = `${window.location.origin}/note/${result.id}` + if (result.password) url += `#${result.password}` function reset() { window.location.reload() diff --git a/packages/frontend/src/lib/ui/Switch.svelte b/packages/frontend/src/lib/ui/Switch.svelte index ec419da..d74be32 100644 --- a/packages/frontend/src/lib/ui/Switch.svelte +++ b/packages/frontend/src/lib/ui/Switch.svelte @@ -4,43 +4,35 @@ export let color = true -
- -
+ From 2e89007c83f851b50f1f181bf6e5ebaef2f95ee6 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Thu, 25 May 2023 10:16:12 +0200 Subject: [PATCH 08/17] add test ids --- packages/frontend/src/lib/ui/AdvancedParameters.svelte | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/frontend/src/lib/ui/AdvancedParameters.svelte b/packages/frontend/src/lib/ui/AdvancedParameters.svelte index 88672e7..7ac306b 100644 --- a/packages/frontend/src/lib/ui/AdvancedParameters.svelte +++ b/packages/frontend/src/lib/ui/AdvancedParameters.svelte @@ -47,8 +47,13 @@ />
- + Date: Thu, 25 May 2023 10:16:44 +0200 Subject: [PATCH 09/17] add --all option, stdin and password option --- packages/cli/src/download.ts | 57 +++++++++++++++++++++--------------- packages/cli/src/index.ts | 6 +++- packages/cli/src/stdin.ts | 22 ++++++++------ 3 files changed, 51 insertions(+), 34 deletions(-) diff --git a/packages/cli/src/download.ts b/packages/cli/src/download.ts index 88a72d1..8fb574f 100644 --- a/packages/cli/src/download.ts +++ b/packages/cli/src/download.ts @@ -7,7 +7,7 @@ import pretty from 'pretty-bytes' import { exit } from './utils' -export async function download(url: URL) { +export async function download(url: URL, all: boolean, suggestedPassword?: string) { setBase(url.origin) const id = url.pathname.split('/')[2] const preview = await info(id).catch(() => exit('Note does not exist or is expired')) @@ -16,14 +16,18 @@ export async function download(url: URL) { let password: string const derivation = preview?.meta.derivation if (derivation) { - const response = await inquirer.prompt([ - { - type: 'password', - message: 'Note password', - name: 'password', - }, - ]) - password = response.password + if (suggestedPassword) { + password = suggestedPassword + } else { + const response = await inquirer.prompt([ + { + type: 'password', + message: 'Note password', + name: 'password', + }, + ]) + password = response.password + } } else { password = url.hash.slice(1) } @@ -39,25 +43,29 @@ export async function download(url: URL) { exit('No files found in note') return } - const { names } = await inquirer.prompt([ - { - type: 'checkbox', - message: 'What files should be saved?', - name: 'names', - choices: files.map((file) => ({ - value: file.name, - name: `${file.name} - ${file.type} - ${pretty(file.size, { binary: true })}`, - checked: true, - })), - }, - ]) - const selected = files.filter((file) => names.includes(file.name)) + let selected: typeof files + if (all) { + selected = files + } else { + const { names } = await inquirer.prompt([ + { + type: 'checkbox', + message: 'What files should be saved?', + name: 'names', + choices: files.map((file) => ({ + value: file.name, + name: `${file.name} - ${file.type} - ${pretty(file.size, { binary: true })}`, + checked: true, + })), + }, + ]) + selected = files.filter((file) => names.includes(file.name)) + } if (!selected.length) exit('No files selected') - await Promise.all( - files.map(async (file) => { + selected.map(async (file) => { let filename = resolve(file.name) try { // If exists -> prepend timestamp to not overwrite the current file @@ -68,6 +76,7 @@ export async function download(url: URL) { console.log(`Saved: ${basename(filename)}`) }) ) + break case 'text': const plaintext = await Adapters.Text.decrypt(note.contents, key).catch(couldNotDecrypt) diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index af86cfe..ee51e45 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -15,6 +15,7 @@ const server = new Option('-s --server ', 'the cryptgeon server to use').de const files = new Argument('', 'Files to be sent').argParser(parseFile) const text = new Argument('', 'Text content of the note') const password = new Option('-p --password ', 'manually set a password') +const all = new Option('-a --all', 'Save all files without prompt').default(false) const url = new Argument('', 'The url to open') const views = new Option('-v --views ', 'Amount of views before getting destroyed').argParser(parseNumber) const minutes = new Option('-m --minutes ', 'Minutes before the note expires').argParser(parseNumber) @@ -86,10 +87,13 @@ send program .command('open') .addArgument(url) + .addOption(password) + .addOption(all) .action(async (note, options) => { try { const url = new URL(note) - await download(url) + options.password ||= await getStdin() + await download(url, options.all, options.password) } catch { exit('Invalid URL') } diff --git a/packages/cli/src/stdin.ts b/packages/cli/src/stdin.ts index d7714e6..d94b46a 100644 --- a/packages/cli/src/stdin.ts +++ b/packages/cli/src/stdin.ts @@ -2,19 +2,23 @@ export function getStdin(timeout: number = 10): Promise { return new Promise((resolve, reject) => { // Store the data from stdin in a buffer let buffer = '' - process.stdin.on('data', (d) => (buffer += d.toString())) + let t: NodeJS.Timeout + + const dataHandler = (d: Buffer) => (buffer += d.toString()) + const endHandler = () => { + clearTimeout(t) + resolve(buffer.trim()) + } // Stop listening for data after the timeout, otherwise hangs indefinitely - const t = setTimeout(() => { - process.stdin.destroy() + t = setTimeout(() => { + process.stdin.removeListener('data', dataHandler) + process.stdin.removeListener('end', endHandler) + process.stdin.pause() resolve('') }, timeout) - // Listen for end and error events - process.stdin.on('end', () => { - clearTimeout(t) - resolve(buffer.trim()) - }) - process.stdin.on('error', reject) + process.stdin.on('data', dataHandler) + process.stdin.on('end', endHandler) }) } From fb95a68b0dbacbf4cdeab3bc381e8037a0398379 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Thu, 25 May 2023 10:17:01 +0200 Subject: [PATCH 10/17] test files in cli and cross with password --- test/cli/file/simple.spec.ts | 33 +++++++++++++++ test/cli/text/simple.spec.ts | 14 +++++- test/cross/file/simple.spec.ts | 53 +++++++++++++++++++++++ test/cross/text/simple.spec.ts | 19 +++++++-- test/files.ts | 25 +++++++++++ test/utils.ts | 73 ++++++++++++++++++++------------ test/web/file/files.ts | 5 --- test/web/file/multiple.spec.ts | 8 ++-- test/web/file/simple.spec.ts | 18 +++++--- test/web/file/too-big.spec.ts | 2 +- test/web/text/expiration.spec.ts | 8 ++-- test/web/text/simple.spec.ts | 13 ++++-- test/web/text/views.spec.ts | 16 +++---- 13 files changed, 225 insertions(+), 62 deletions(-) create mode 100644 test/cli/file/simple.spec.ts create mode 100644 test/cross/file/simple.spec.ts create mode 100644 test/files.ts delete mode 100644 test/web/file/files.ts diff --git a/test/cli/file/simple.spec.ts b/test/cli/file/simple.spec.ts new file mode 100644 index 0000000..ac49191 --- /dev/null +++ b/test/cli/file/simple.spec.ts @@ -0,0 +1,33 @@ +import { test } from '@playwright/test' +import { basename } from 'node:path' +import { Files, getFileChecksum, rm, tmpFile } from '../../files' +import { CLI, getLinkFromCLI } from '../../utils' + +test.describe('file @cli', () => { + test('simple', async ({ page }) => { + const file = await tmpFile(Files.Image) + const checksum = await getFileChecksum(file) + const note = await CLI('send', 'file', file) + const link = getLinkFromCLI(note.stdout) + await rm(file) + + await CLI('open', link, '--all') + const c = await getFileChecksum(basename(file)) + await rm(basename(file)) + test.expect(checksum).toBe(c) + }) + + test('simple with password', async ({ page }) => { + const file = await tmpFile(Files.Image) + const password = 'password' + const checksum = await getFileChecksum(file) + const note = await CLI('send', 'file', file, '--password', password) + const link = getLinkFromCLI(note.stdout) + await rm(file) + + await CLI('open', link, '--all', '--password', password) + const c = await getFileChecksum(basename(file)) + await rm(basename(file)) + test.expect(checksum).toBe(c) + }) +}) diff --git a/test/cli/text/simple.spec.ts b/test/cli/text/simple.spec.ts index 0434f25..6286b4b 100644 --- a/test/cli/text/simple.spec.ts +++ b/test/cli/text/simple.spec.ts @@ -1,13 +1,23 @@ import { test } from '@playwright/test' -import { CLI } from '../../utils' +import { CLI, getLinkFromCLI } from '../../utils' test.describe('text @cli', () => { test('simple', async ({ page }) => { const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.` const note = await CLI('send', 'text', text) - const link = note.stdout.trim().replace(/(.|\s)*http/g, 'http') + const link = getLinkFromCLI(note.stdout) const retrieved = await CLI('open', link) test.expect(retrieved.stdout.trim()).toBe(text) }) + + test('simple with password', async ({ page }) => { + const text = `Endless prejudice endless play derive joy eternal-return selfish burying.` + const password = 'password' + const note = await CLI('send', 'text', text, '--password', password) + const link = getLinkFromCLI(note.stdout) + + const retrieved = await CLI('open', link, '--password', password) + test.expect(retrieved.stdout.trim()).toBe(text) + }) }) diff --git a/test/cross/file/simple.spec.ts b/test/cross/file/simple.spec.ts new file mode 100644 index 0000000..103d29c --- /dev/null +++ b/test/cross/file/simple.spec.ts @@ -0,0 +1,53 @@ +import { test } from '@playwright/test' +import { CLI, checkLinkForDownload, checkLinkForText, createNote, getLinkFromCLI } from '../../utils' +import { Files, getFileChecksum, rm, tmpFile } from '../../files' +import { basename } from 'path' + +const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.` +const password = 'password' + +test.describe('text @cross', () => { + test('cli to web', async ({ page }) => { + const file = await tmpFile(Files.Image) + const checksum = await getFileChecksum(file) + const note = await CLI('send', 'file', file) + const link = getLinkFromCLI(note.stdout) + await rm(file) + + await checkLinkForDownload(page, { link, text: basename(file), checksum }) + }) + + test('cli to web with password', async ({ page }) => { + const file = await tmpFile(Files.Image) + const checksum = await getFileChecksum(file) + const note = await CLI('send', 'file', file, '--password', password) + const link = getLinkFromCLI(note.stdout) + await rm(file) + + await checkLinkForDownload(page, { link, text: basename(file), checksum, password }) + }) + + test('web to cli', async ({ page }) => { + const files = [Files.Image] + const checksum = await getFileChecksum(files[0]) + const link = await createNote(page, { files }) + + const filename = basename(files[0]) + await CLI('open', link, '--all') + const c = await getFileChecksum(filename) + await rm(basename(filename)) + test.expect(checksum).toBe(c) + }) + + test('web to cli with password', async ({ page }) => { + const files = [Files.Image] + const checksum = await getFileChecksum(files[0]) + const link = await createNote(page, { files, password }) + + const filename = basename(files[0]) + await CLI('open', link, '--all', '--password', password) + const c = await getFileChecksum(filename) + await rm(basename(filename)) + test.expect(checksum).toBe(c) + }) +}) diff --git a/test/cross/text/simple.spec.ts b/test/cross/text/simple.spec.ts index 676c0c0..02f52e6 100644 --- a/test/cross/text/simple.spec.ts +++ b/test/cross/text/simple.spec.ts @@ -1,14 +1,15 @@ import { test } from '@playwright/test' -import { CLI, checkLinkForText, createNote } from '../../utils' +import { CLI, checkLinkForText, createNote, getLinkFromCLI } from '../../utils' const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.` +const password = 'password' test.describe('text @cross', () => { test('cli to web', async ({ page }) => { const note = await CLI('send', 'text', text) - const link = note.stdout.trim().replace(/(.|\s)*http/g, 'http') + const link = getLinkFromCLI(note.stdout) - await checkLinkForText(page, link, text) + await checkLinkForText(page, { link, text }) }) test('web to cli', async ({ page }) => { @@ -16,4 +17,16 @@ test.describe('text @cross', () => { const retrieved = await CLI('open', link) test.expect(retrieved.stdout.trim()).toBe(text) }) + + test('cli to web with password', async ({ page }) => { + const note = await CLI('send', 'text', text, '--password', password) + const link = getLinkFromCLI(note.stdout) + await checkLinkForText(page, { link, text, password }) + }) + + test('web to cli with password', async ({ page }) => { + const link = await createNote(page, { text, password }) + const retrieved = await CLI('open', link, '--password', password) + test.expect(retrieved.stdout.trim()).toBe(text) + }) }) diff --git a/test/files.ts b/test/files.ts new file mode 100644 index 0000000..4ef84e0 --- /dev/null +++ b/test/files.ts @@ -0,0 +1,25 @@ +import { createHash } from 'crypto' +import { cp as cpFN, rm as rmFN } from 'fs' +import { readFile } from 'fs/promises' +import { promisify } from 'util' + +export const cp = promisify(cpFN) +export const rm = promisify(rmFN) + +export const Files = { + PDF: 'test/assets/AES.pdf', + Image: 'test/assets/image.jpg', + Zip: 'test/assets/Pigeons.zip', +} + +export async function getFileChecksum(file: string) { + const buffer = await readFile(file) + const hash = createHash('sha3-256').update(buffer).digest('hex') + return hash +} + +export async function tmpFile(file: string) { + const name = `./tmp/${Math.random().toString(36).substring(7)}` + await cp(file, name) + return name +} diff --git a/test/utils.ts b/test/utils.ts index 99d01d8..d8c3846 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -1,19 +1,25 @@ import { expect, type Page } from '@playwright/test' -import { createHash } from 'crypto' -import { readFile } from 'fs/promises' import { execFile } from 'node:child_process' import { promisify } from 'node:util' +import { getFileChecksum } from './files' const exec = promisify(execFile) -type CreatePage = { text?: string; files?: string[]; views?: number; expiration?: number; error?: string } +type CreatePage = { + text?: string + files?: string[] + views?: number + expiration?: number + error?: string + password?: string +} export async function createNote(page: Page, options: CreatePage): Promise { await page.goto('/') if (options.text) { - await page.locator('[data-testid="text-field"]').fill(options.text) + await page.getByTestId('text-field').fill(options.text) } else if (options.files) { - await page.locator('[data-testid="switch-file"]').click() + await page.getByTestId('switch-file').click() const [fileChooser] = await Promise.all([ page.waitForEvent('filechooser'), @@ -22,13 +28,16 @@ export async function createNote(page: Page, options: CreatePage): Promise> text=${text}`).click(), + page.getByTestId(`result`).locator(`text=${options.text}`).click(), ]) const path = await download.path() if (!path) throw new Error('Download failed') const cs = await getFileChecksum(path) - await expect(cs).toBe(checksum) + await expect(cs).toBe(options.checksum) } -export async function checkLinkForText(page: Page, link: string, text: string) { + +export async function checkLinkForText(page: Page, options: CheckLinkBase) { await page.goto('/') - await page.goto(link) - await page.locator('[data-testid="show-note-button"]').click() - await expect(await page.locator('[data-testid="result"] >> .note').innerText()).toContain(text) + await page.goto(options.link) + if (options.password) await page.getByTestId('show-note-password').fill(options.password) + await page.getByTestId('show-note-button').click() + const text = await page.getByTestId('result').locator('.note').innerText() + await expect(text).toContain(options.text) } export async function checkLinkDoesNotExist(page: Page, link: string) { @@ -68,12 +87,6 @@ export async function checkLinkDoesNotExist(page: Page, link: string) { await expect(page.locator('main')).toContainText('note was not found or was already deleted') } -export async function getFileChecksum(file: string) { - const buffer = await readFile(file) - const hash = createHash('sha3-256').update(buffer).digest('hex') - return hash -} - export async function CLI(...args: string[]) { return await exec('./packages/cli/dist/index.cjs', args, { env: { @@ -82,3 +95,9 @@ export async function CLI(...args: string[]) { }, }) } + +export function getLinkFromCLI(output: string): string { + const match = output.match(/(https?:\/\/[^\s]+)/) + if (!match) throw new Error('No link found in CLI output') + return match[0] +} diff --git a/test/web/file/files.ts b/test/web/file/files.ts deleted file mode 100644 index 6f99fe9..0000000 --- a/test/web/file/files.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default { - PDF: 'test/assets/AES.pdf', - Image: 'test/assets/image.jpg', - Zip: 'test/assets/Pigeons.zip', -} diff --git a/test/web/file/multiple.spec.ts b/test/web/file/multiple.spec.ts index d3082fe..fea086d 100644 --- a/test/web/file/multiple.spec.ts +++ b/test/web/file/multiple.spec.ts @@ -1,13 +1,13 @@ import { test } from '@playwright/test' -import { checkLinkForDownload, createNote, getFileChecksum } from '../../utils' -import Files from './files' +import { Files, getFileChecksum } from '../../files' +import { checkLinkForDownload, createNote } from '../../utils' test.describe('@web', () => { test('multiple', async ({ page }) => { const files = [Files.PDF, Files.Image] const checksums = await Promise.all(files.map(getFileChecksum)) const link = await createNote(page, { files, views: 2 }) - await checkLinkForDownload(page, link, 'image.jpg', checksums[1]) - await checkLinkForDownload(page, link, 'AES.pdf', checksums[0]) + await checkLinkForDownload(page, { link, text: 'image.jpg', checksum: checksums[1] }) + await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum: checksums[0] }) }) }) diff --git a/test/web/file/simple.spec.ts b/test/web/file/simple.spec.ts index 35fd5c7..00a600d 100644 --- a/test/web/file/simple.spec.ts +++ b/test/web/file/simple.spec.ts @@ -1,12 +1,12 @@ import { test } from '@playwright/test' -import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote, getFileChecksum } from '../../utils' -import Files from './files' +import { Files, getFileChecksum } from '../../files' +import { checkLinkDoesNotExist, checkLinkForDownload, checkLinkForText, createNote } from '../../utils' test.describe('@web', () => { test('simple pdf', async ({ page }) => { const files = [Files.PDF] const link = await createNote(page, { files }) - await checkLinkForText(page, link, 'AES.pdf') + await checkLinkForText(page, { link, text: 'AES.pdf' }) await checkLinkDoesNotExist(page, link) }) @@ -14,13 +14,21 @@ test.describe('@web', () => { const files = [Files.PDF] const checksum = await getFileChecksum(files[0]) const link = await createNote(page, { files }) - await checkLinkForDownload(page, link, 'AES.pdf', checksum) + await checkLinkForDownload(page, { link, text: 'AES.pdf', checksum }) }) test('image content', async ({ page }) => { const files = [Files.Image] const checksum = await getFileChecksum(files[0]) const link = await createNote(page, { files }) - await checkLinkForDownload(page, link, 'image.jpg', checksum) + await checkLinkForDownload(page, { link, text: 'image.jpg', checksum }) + }) + + test('simple pdf with password', async ({ page }) => { + const files = [Files.PDF] + const password = 'password' + const link = await createNote(page, { files, password }) + await checkLinkForText(page, { link, text: 'AES.pdf', password }) + await checkLinkDoesNotExist(page, link) }) }) diff --git a/test/web/file/too-big.spec.ts b/test/web/file/too-big.spec.ts index c2f4794..03ceec6 100644 --- a/test/web/file/too-big.spec.ts +++ b/test/web/file/too-big.spec.ts @@ -1,6 +1,6 @@ import { test } from '@playwright/test' import { createNote } from '../../utils' -import Files from './files' +import { Files } from '../../files' test.describe('@web', () => { test.skip('to big zip', async ({ page }) => { diff --git a/test/web/text/expiration.spec.ts b/test/web/text/expiration.spec.ts index 44a6b24..6ff3e4d 100644 --- a/test/web/text/expiration.spec.ts +++ b/test/web/text/expiration.spec.ts @@ -7,10 +7,10 @@ test.describe('@web', () => { const minutes = 1 const timeout = minutes * 60_000 test.setTimeout(timeout * 2) - const shareLink = await createNote(page, { text, expiration: minutes }) - await checkLinkForText(page, shareLink, text) - await checkLinkForText(page, shareLink, text) + const link = await createNote(page, { text, expiration: minutes }) + await checkLinkForText(page, { link, text }) + await checkLinkForText(page, { link, text }) await page.waitForTimeout(timeout) - await checkLinkDoesNotExist(page, shareLink) + await checkLinkDoesNotExist(page, link) }) }) diff --git a/test/web/text/simple.spec.ts b/test/web/text/simple.spec.ts index a26f5db..ef144c2 100644 --- a/test/web/text/simple.spec.ts +++ b/test/web/text/simple.spec.ts @@ -3,8 +3,15 @@ import { checkLinkForText, createNote } from '../../utils' test.describe('@web', () => { test('simple', async ({ page }) => { - const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of decieve play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.` - const shareLink = await createNote(page, { text }) - await checkLinkForText(page, shareLink, text) + const text = `Endless prejudice endless play derive joy eternal-return selfish burying. Of deceive play pinnacle faith disgust. Spirit reason salvation burying strong of joy ascetic selfish against merciful sea truth. Ubermensch moral prejudice derive chaos mountains ubermensch justice philosophy justice ultimate joy ultimate transvaluation. Virtues convictions war ascetic eternal-return spirit. Ubermensch transvaluation noble revaluation sexuality intentions salvation endless decrepit hope noble fearful. Justice ideal ultimate snare god joy evil sexuality insofar gains oneself ideal.` + const link = await createNote(page, { text }) + await checkLinkForText(page, { link, text }) + }) + + test('simple with password', async ({ page }) => { + const text = 'Foo bar' + const password = '123' + const shareLink = await createNote(page, { text, password }) + await checkLinkForText(page, { link: shareLink, text, password }) }) }) diff --git a/test/web/text/views.spec.ts b/test/web/text/views.spec.ts index 524fa5d..48a9c98 100644 --- a/test/web/text/views.spec.ts +++ b/test/web/text/views.spec.ts @@ -4,17 +4,17 @@ import { checkLinkDoesNotExist, checkLinkForText, createNote } from '../../utils test.describe('@web', () => { test('only shown once', async ({ page }) => { const text = `Christian victorious reason suicide dead. Right ultimate gains god hope truth burying selfish society joy. Ultimate.` - const shareLink = await createNote(page, { text }) - await checkLinkForText(page, shareLink, text) - await checkLinkDoesNotExist(page, shareLink) + const link = await createNote(page, { text }) + await checkLinkForText(page, { link, text }) + await checkLinkDoesNotExist(page, link) }) test('view 3 times', async ({ page }) => { const text = `Justice holiest overcome fearful strong ultimate holiest christianity.` - const shareLink = await createNote(page, { text, views: 3 }) - await checkLinkForText(page, shareLink, text) - await checkLinkForText(page, shareLink, text) - await checkLinkForText(page, shareLink, text) - await checkLinkDoesNotExist(page, shareLink) + const link = await createNote(page, { text, views: 3 }) + await checkLinkForText(page, { link, text }) + await checkLinkForText(page, { link, text }) + await checkLinkForText(page, { link, text }) + await checkLinkDoesNotExist(page, link) }) }) From a5809c216c2b401f7ae0aed28b1d6bef0abca072 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Thu, 25 May 2023 10:17:08 +0200 Subject: [PATCH 11/17] fix scripts --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e6fb6d7..756cbae 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "docker:up": "docker compose -f docker-compose.dev.yaml up", "docker:build": "docker compose -f docker-compose.dev.yaml build", "test": "playwright test --project chrome firefox safari", - "test:local": "playwright test --project local", + "test:local": "playwright test --project chrome", "test:server": "run-s docker:up", "test:prepare": "run-p build docker:build", "build": "pnpm run --recursive --filter=!@cryptgeon/backend build" From 80e64ad207d525065db5ab21296caf9359be5898 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Thu, 25 May 2023 18:15:05 +0200 Subject: [PATCH 12/17] fix types --- .../frontend/src/lib/ui/AdvancedParameters.svelte | 11 ++++++----- packages/frontend/src/lib/views/Create.svelte | 7 ++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/frontend/src/lib/ui/AdvancedParameters.svelte b/packages/frontend/src/lib/ui/AdvancedParameters.svelte index 7ac306b..e3d18f4 100644 --- a/packages/frontend/src/lib/ui/AdvancedParameters.svelte +++ b/packages/frontend/src/lib/ui/AdvancedParameters.svelte @@ -8,10 +8,11 @@ export let note: Note export let timeExpiration = false + export let customPassword: string | null = null - let customPassword = false + let hasCustomPassword = false - $: if (!customPassword) note.password = undefined + $: if (!hasCustomPassword) customPassword = null
@@ -49,15 +50,15 @@
diff --git a/packages/frontend/src/lib/views/Create.svelte b/packages/frontend/src/lib/views/Create.svelte index debb0d0..80808a3 100644 --- a/packages/frontend/src/lib/views/Create.svelte +++ b/packages/frontend/src/lib/views/Create.svelte @@ -27,6 +27,7 @@ let advanced = false let isFile = false let timeExpiration = false + let customPassword: string | null = null let description = '' let loading: string | null = null @@ -57,7 +58,7 @@ try { loading = $t('common.encrypting') - const derived = note.password && (await AES.derive(note.password)) + const derived = customPassword && (await AES.derive(customPassword)) const key = derived ? derived[0] : await AES.generateKey() const data: Note = { @@ -79,7 +80,7 @@ const response = await create(data) result = { id: response.id, - password: note.password ? undefined : Hex.encode(key), + password: customPassword ? undefined : Hex.encode(key), } notify.success($t('home.messages.note_created')) } catch (e) { @@ -150,7 +151,7 @@ {#if advanced}

- +
{/if} From 3c86f3f3be49ad21cf40fc15b4b6a777facf7649 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Thu, 25 May 2023 18:15:18 +0200 Subject: [PATCH 13/17] update pnpm version --- cryptgeon.code-workspace | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cryptgeon.code-workspace b/cryptgeon.code-workspace index be05d60..d659d68 100644 --- a/cryptgeon.code-workspace +++ b/cryptgeon.code-workspace @@ -17,7 +17,7 @@ } ], "settings": { - "i18n-ally.localesPaths": ["packages/frontend/locales"], + "i18n-ally.localesPaths": ["locales"], "cSpell.words": ["cryptgeon"] } } diff --git a/package.json b/package.json index 756cbae..8f69d99 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "packageManager": "pnpm@8.4.0", + "packageManager": "pnpm@8.5.1", "scripts": { "dev:docker": "docker-compose -f docker-compose.dev.yaml up redis", "dev:packages": "pnpm --parallel run dev", From 83b2fa537293a800c40293ad0c1c45ab492e999f Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Thu, 25 May 2023 18:15:31 +0200 Subject: [PATCH 14/17] version bump --- packages/backend/Cargo.lock | 2 +- packages/backend/Cargo.toml | 2 +- packages/cli/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/Cargo.lock b/packages/backend/Cargo.lock index 898d59c..92c4482 100644 --- a/packages/backend/Cargo.lock +++ b/packages/backend/Cargo.lock @@ -439,7 +439,7 @@ dependencies = [ [[package]] name = "cryptgeon" -version = "2.3.0-beta.4" +version = "2.3.0-beta.3" dependencies = [ "actix-files", "actix-web", diff --git a/packages/backend/Cargo.toml b/packages/backend/Cargo.toml index 00ffa0d..9965126 100644 --- a/packages/backend/Cargo.toml +++ b/packages/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cryptgeon" -version = "2.3.0-beta.4" +version = "2.3.0-beta.3" authors = ["cupcakearmy "] edition = "2021" diff --git a/packages/cli/package.json b/packages/cli/package.json index df7f5bf..38c5361 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,5 +1,5 @@ { - "version": "2.3.0-beta.4", + "version": "2.3.0-beta.3", "name": "cryptgeon", "type": "module", "engines": { From ac68f4a540942306a9308468787054a9270479c8 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Thu, 25 May 2023 19:06:07 +0200 Subject: [PATCH 15/17] docs --- README.md | 25 +++++++++++++----- packages/cli/README.md | 54 +++++++++++++++++++++++++++++++++++++++ packages/cli/src/index.ts | 4 ++- 3 files changed, 75 insertions(+), 8 deletions(-) create mode 100644 packages/cli/README.md diff --git a/README.md b/README.md index 8dfca81..e3d8dfa 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,8 @@ EN | [简体中文](README_zh-CN.md) ## About? -_cryptgeon_ is a secure, open source sharing note or file service inspired by [_PrivNote_](https://privnote.com) +_cryptgeon_ is a secure, open source sharing note or file service inspired by [_PrivNote_](https://privnote.com). +It includes a server, a web page and a CLI client. > 🌍 If you want to translate the project feel free to reach out to me. > @@ -26,10 +27,21 @@ _cryptgeon_ is a secure, open source sharing note or file service inspired by [_ ## Live Service / Demo +### Web + Check out the live service / demo and see for yourself [cryptgeon.org](https://cryptgeon.org) +### CLI + +``` +npx cryptgeon send text "This is a secret note" +``` + +For more documentation about the CLI see the [readme](./packages/cli/README.md). + ## Features +- send text or files - server cannot decrypt contents due to client side encryption - view or time constraints - in memory, no persistence @@ -121,14 +133,13 @@ There is a [guide](https://mariushosting.com/how-to-install-cryptgeon-on-your-sy **Requirements** - `pnpm`: `>=6` -- `node`: `>=16` +- `node`: `>=18` - `rust`: edition `2021` **Install** ```bash pnpm install -pnpm --prefix frontend install # Also you need cargo watch if you don't already have it installed. # https://lib.rs/crates/cargo-watch @@ -148,6 +159,7 @@ Running `pnpm run dev` in the root folder will start the following things: - redis docker container - rust backend - client +- cli You can see the app under [localhost:1234](http://localhost:1234). @@ -157,10 +169,7 @@ Tests are end to end tests written with Playwright. ```sh pnpm run test:prepare -docker compose up redis -d -pnpm run test:server -# In another terminal. # Use the test or test:local script. The local version only runs in one browser for quicker development. pnpm run test:local ``` @@ -169,7 +178,9 @@ pnpm run test:local Please refer to the security section [here](./SECURITY.md). -###### Attributions +--- + +_Attributions_ - Test data: - Text for tests [Nietzsche Ipsum](https://nietzsche-ipsum.com/) diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..4af3a42 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,54 @@ +# Cryptgeon CLI + +The CLI is a functionally identical way to interact with cryptgeon notes. +It supports text, files, expiration, password, etc. + +## Installation + +```bash +npx cryptgeon + +# Or install globally +npm -g install cryptgeon +cryptgeon +``` + +## Examples + +```bash +# Create simple note +cryptgeon send text "Foo bar" + +# Send two files +cryptgeon send file my.pdf picture.png + +# 3 views +cryptgeon send text "My message" --views 3 + +# 10 minutes +cryptgeon send text "My message" --minutes 10 + +# Custom password +cryptgeon send text "My message" --password "1337" + +# Password from stdin +echo "1337" | cryptgeon send text "My message" + +# Open a link +cryptgeon open https://cryptgeon.org/note/16gOIkxWjCxYNuXM8tCqMUzl... +``` + +## Options + +### Custom server + +The default server is `cryptgeon.org`, however you can use any cryptgeon server by passing the `-s` or `--server` option, or by setting the `CRYPTGEON_SERVER` environment variable. + +### Password + +Optionally, just like in the web ui, you can choose to use a manual password. You can do that by passing the `-p` or `--password` options, or by piping it into stdin. + +```bash +echo "my pw" | cryptgeon send text "my text" +cat pass.txt | cryptgeon send text "my text" +``` diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ee51e45..cf4ef00 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -42,6 +42,7 @@ program.name('cryptgeon').version(version).configureHelp({ showGlobalOptions: tr program .command('info') + .description('show information about the server') .addOption(server) .action(async (options) => { setBase(options.server) @@ -56,7 +57,7 @@ program console.table(formatted) }) -const send = program.command('send') +const send = program.command('send').description('send a note') send .command('file') .addArgument(files) @@ -86,6 +87,7 @@ send program .command('open') + .description('open a link with text or files inside') .addArgument(url) .addOption(password) .addOption(all) From 92893a5b2dc50dfa5e5db86486079bc53f21005a Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Thu, 25 May 2023 23:29:05 +0200 Subject: [PATCH 16/17] github actions --- .github/workflows/release.yml | 8 +++++++- .github/workflows/test.yaml | 3 ++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1dea9a7..510375b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,14 +12,20 @@ jobs: steps: - uses: actions/checkout@v3 + - uses: pnpm/action-setup@v2 - uses: actions/setup-node@v3 with: + cache: 'pnpm' node-version-file: '.nvmrc' - - uses: pnpm/action-setup@v2 + registry-url: 'https://registry.npmjs.org' - run: | pnpm install --frozen-lockfile pnpm run build + + - run: npm publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} docker: runs-on: ubuntu-latest diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index a4a3114..095dd35 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,10 +13,11 @@ jobs: - uses: actions/checkout@v3 # Node + - uses: pnpm/action-setup@v2 - uses: actions/setup-node@v3 with: + cache: 'pnpm' node-version-file: '.nvmrc' - - uses: pnpm/action-setup@v2 # Docker - uses: docker/setup-qemu-action@v2 From 9c9c23d958be6b057bd7a077e6669179532dc4fc Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Thu, 25 May 2023 23:29:09 +0200 Subject: [PATCH 17/17] version bump --- packages/backend/Cargo.lock | 2 +- packages/backend/Cargo.toml | 2 +- packages/cli/package.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/backend/Cargo.lock b/packages/backend/Cargo.lock index 92c4482..898d59c 100644 --- a/packages/backend/Cargo.lock +++ b/packages/backend/Cargo.lock @@ -439,7 +439,7 @@ dependencies = [ [[package]] name = "cryptgeon" -version = "2.3.0-beta.3" +version = "2.3.0-beta.4" dependencies = [ "actix-files", "actix-web", diff --git a/packages/backend/Cargo.toml b/packages/backend/Cargo.toml index 9965126..00ffa0d 100644 --- a/packages/backend/Cargo.toml +++ b/packages/backend/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cryptgeon" -version = "2.3.0-beta.3" +version = "2.3.0-beta.4" authors = ["cupcakearmy "] edition = "2021" diff --git a/packages/cli/package.json b/packages/cli/package.json index 38c5361..df7f5bf 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,5 +1,5 @@ { - "version": "2.3.0-beta.3", + "version": "2.3.0-beta.4", "name": "cryptgeon", "type": "module", "engines": {