From f60951e7d58b7d7d3856a201abb73b2c7806000f Mon Sep 17 00:00:00 2001 From: cupcakearmy Date: Mon, 18 Jul 2022 23:49:13 +0200 Subject: [PATCH] use base64 instead of hex and refactor a bit --- frontend/src/lib/adapters.ts | 61 ++++++++++++++ frontend/src/lib/api.ts | 4 + frontend/src/lib/crypto.ts | 112 ++++++++++++++++---------- frontend/src/lib/files.ts | 13 --- frontend/src/lib/ui/FileUpload.svelte | 38 ++++----- frontend/src/lib/ui/NoteResult.svelte | 36 +++++++++ frontend/src/lib/ui/ShowNote.svelte | 16 ++-- frontend/src/lib/ui/TextInput.svelte | 7 +- frontend/src/lib/views/Create.svelte | 69 ++++++++-------- frontend/src/routes/__layout.svelte | 2 +- frontend/src/routes/note/[id].svelte | 44 +++++++--- 11 files changed, 266 insertions(+), 136 deletions(-) create mode 100644 frontend/src/lib/adapters.ts delete mode 100644 frontend/src/lib/files.ts create mode 100644 frontend/src/lib/ui/NoteResult.svelte diff --git a/frontend/src/lib/adapters.ts b/frontend/src/lib/adapters.ts new file mode 100644 index 0000000..5d2c91e --- /dev/null +++ b/frontend/src/lib/adapters.ts @@ -0,0 +1,61 @@ +import type { EncryptedFileDTO, FileDTO } from './api' +import { Crypto } from './crypto' + +abstract class CryptAdapter { + abstract encrypt(plaintext: T, key: CryptoKey): Promise + abstract decrypt(ciphertext: string, key: CryptoKey): Promise +} + +class CryptTextAdapter implements CryptAdapter { + async encrypt(plaintext: string, key: CryptoKey) { + return await Crypto.encrypt(new TextEncoder().encode(plaintext), key) + } + async decrypt(ciphertext: string, key: CryptoKey) { + const plaintext = await Crypto.decrypt(ciphertext, key) + return new TextDecoder().decode(plaintext) + } +} + +class CryptBlobAdapter implements CryptAdapter { + async encrypt(plaintext: Blob, key: CryptoKey) { + return await Crypto.encrypt(await plaintext.arrayBuffer(), key) + } + + async decrypt(ciphertext: string, key: CryptoKey) { + const plaintext = await Crypto.decrypt(ciphertext, key) + return new Blob([plaintext], { type: 'application/octet-stream' }) + } +} + +class CryptFilesAdapter implements CryptAdapter { + async encrypt(plaintext: FileDTO[], key: CryptoKey) { + const adapter = new CryptBlobAdapter() + const data: Promise[] = plaintext.map(async (file) => ({ + name: file.name, + size: file.size, + type: file.type, + contents: await adapter.encrypt(file.contents, key), + })) + return JSON.stringify(await Promise.all(data)) + } + + async decrypt(ciphertext: string, key: CryptoKey) { + const adapter = new CryptBlobAdapter() + const data: EncryptedFileDTO[] = JSON.parse(ciphertext) + const files: FileDTO[] = await Promise.all( + data.map(async (file) => ({ + name: file.name, + size: file.size, + type: file.type, + contents: await adapter.decrypt(file.contents, key), + })) + ) + return files + } +} + +export const Adapters = { + Text: new CryptTextAdapter(), + Blob: new CryptBlobAdapter(), + Files: new CryptFilesAdapter(), +} diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 11d3afc..793753d 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -11,6 +11,10 @@ export type NotePublic = Pick export type NoteCreate = Omit & { meta: string } export type FileDTO = Pick & { + contents: Blob +} + +export type EncryptedFileDTO = Omit & { contents: string } diff --git a/frontend/src/lib/crypto.ts b/frontend/src/lib/crypto.ts index c64c879..4a4bab5 100644 --- a/frontend/src/lib/crypto.ts +++ b/frontend/src/lib/crypto.ts @@ -19,53 +19,79 @@ export class Hex { } } -const ALG = 'AES-GCM' +export class ArrayBufferUtils { + static async toString(buffer: ArrayBuffer): Promise { + const reader = new window.FileReader() + reader.readAsDataURL(new Blob([buffer])) + return new Promise((resolve) => { + reader.onloadend = () => resolve(reader.result as string) + }) + } -export function getRandomBytes(size = 16): Uint8Array { - return window.crypto.getRandomValues(new Uint8Array(size)) + static async fromString(s: string): Promise { + return fetch(s) + .then((r) => r.blob()) + .then((b) => b.arrayBuffer()) + } } -export function getKeyFromString(password: string) { - return window.crypto.subtle.importKey( - 'raw', - new TextEncoder().encode(password), - 'PBKDF2', - false, - ['deriveBits', 'deriveKey'] - ) -} +export class Crypto { + private static ALG = 'AES-GCM' + private static DELIMITER = ':::' -export async function getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) { - const iterations = 100_000 - return window.crypto.subtle.deriveKey( - { - name: 'PBKDF2', - salt, - iterations, - hash: 'SHA-512', - }, - key, - { name: ALG, length: 256 }, - true, - ['encrypt', 'decrypt'] - ) -} + public static getRandomBytes(size: number): Uint8Array { + return window.crypto.getRandomValues(new Uint8Array(size)) + } -export async function encrypt(plaintext: string, key: CryptoKey) { - const salt = getRandomBytes(16) - const derived = await getDerivedForKey(key, salt) - const iv = getRandomBytes(16) - const encrypted = await window.crypto.subtle.encrypt( - { name: ALG, iv }, - derived, - new TextEncoder().encode(plaintext) - ) - return [salt, iv, encrypted].map(Hex.encode).join(':') -} + public static getKeyFromString(password: string) { + return window.crypto.subtle.importKey( + 'raw', + new TextEncoder().encode(password), + 'PBKDF2', + false, + ['deriveBits', 'deriveKey'] + ) + } + public static async getDerivedForKey(key: CryptoKey, salt: ArrayBuffer) { + const iterations = 100_000 + return window.crypto.subtle.deriveKey( + { + name: 'PBKDF2', + salt, + iterations, + hash: 'SHA-512', + }, + key, + { name: this.ALG, length: 256 }, + true, + ['encrypt', 'decrypt'] + ) + } -export async function decrypt(ciphertext: string, key: CryptoKey) { - const [salt, iv, encrypted] = ciphertext.split(':').map(Hex.decode) - const derived = await getDerivedForKey(key, salt) - const plaintext = await window.crypto.subtle.decrypt({ name: ALG, iv }, derived, encrypted) - return new TextDecoder().decode(plaintext) + public static async encrypt(plaintext: ArrayBuffer, key: CryptoKey): Promise { + const salt = this.getRandomBytes(16) + const derived = await this.getDerivedForKey(key, salt) + const iv = this.getRandomBytes(16) + const encrypted: ArrayBuffer = await window.crypto.subtle.encrypt( + { name: this.ALG, iv }, + derived, + plaintext + ) + const data = [ + Hex.encode(salt), + Hex.encode(iv), + await ArrayBufferUtils.toString(encrypted), + ].join(this.DELIMITER) + return data + } + + public static async decrypt(ciphertext: string, key: CryptoKey): Promise { + const splitted = ciphertext.split(this.DELIMITER) + const salt = Hex.decode(splitted[0]) + const iv = Hex.decode(splitted[1]) + const encrypted = await ArrayBufferUtils.fromString(splitted[2]) + const derived = await this.getDerivedForKey(key, salt) + const plaintext = await window.crypto.subtle.decrypt({ name: this.ALG, iv }, derived, encrypted) + return plaintext + } } diff --git a/frontend/src/lib/files.ts b/frontend/src/lib/files.ts deleted file mode 100644 index 78a31d2..0000000 --- a/frontend/src/lib/files.ts +++ /dev/null @@ -1,13 +0,0 @@ -export class Files { - static toString(f: File | Blob): Promise { - const reader = new window.FileReader() - reader.readAsDataURL(f) - return new Promise((resolve) => { - reader.onloadend = () => resolve(reader.result as string) - }) - } - - static async fromString(s: string): Promise { - return fetch(s).then((r) => r.blob()) - } -} diff --git a/frontend/src/lib/ui/FileUpload.svelte b/frontend/src/lib/ui/FileUpload.svelte index f158349..24687b9 100644 --- a/frontend/src/lib/ui/FileUpload.svelte +++ b/frontend/src/lib/ui/FileUpload.svelte @@ -1,38 +1,32 @@ @@ -57,7 +51,9 @@
{$t('file_upload.no_files_selected')}
- {$t('common.max')}: + + {$t('common.max')}: +
{/if} diff --git a/frontend/src/lib/ui/NoteResult.svelte b/frontend/src/lib/ui/NoteResult.svelte new file mode 100644 index 0000000..b1d1b41 --- /dev/null +++ b/frontend/src/lib/ui/NoteResult.svelte @@ -0,0 +1,36 @@ + + + + + +
+

+ {@html $t('home.new_note_notice')} +

+
+ + + diff --git a/frontend/src/lib/ui/ShowNote.svelte b/frontend/src/lib/ui/ShowNote.svelte index 7d9a3bf..96793b6 100644 --- a/frontend/src/lib/ui/ShowNote.svelte +++ b/frontend/src/lib/ui/ShowNote.svelte @@ -1,20 +1,24 @@ + + {#if result} - -
-

- {@html $t('home.new_note_notice')} -

-
- + {:else}

{@html $status?.theme_text || $t('home.intro')}

- {#if file} - (note.contents = f.detail)} /> + {#if isFile} + {:else}