From e6d1e0f44ae5665377f986bac1ae8f0c6aed2689 Mon Sep 17 00:00:00 2001 From: Niccolo Borgioli Date: Tue, 23 May 2023 09:39:00 +0200 Subject: [PATCH] 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' } }) -}