From 31081e51910863ed7c64d47c0a947cb6d5678493 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 2 Jan 2025 08:49:51 +0900 Subject: [PATCH] =?UTF-8?q?=EB=94=94=EB=A0=89=ED=84=B0=EB=A6=AC=20?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=A7=80=20=EB=A0=88=EC=9D=B4=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20=EB=94=94=EB=A0=89?= =?UTF-8?q?=ED=84=B0=EB=A6=AC=20=EC=83=9D=EC=84=B1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/components/BottomSheet.svelte | 4 +- src/lib/components/Modal.svelte | 22 +++-- src/lib/components/buttons/Button.svelte | 4 +- src/lib/components/buttons/EntryButton.svelte | 30 ++++++ .../components/buttons/FloatingButton.svelte | 22 +++++ src/lib/components/buttons/index.ts | 2 + src/lib/components/divs/AdaptiveDiv.svelte | 2 +- src/lib/components/divs/TitleDiv.svelte | 5 +- src/lib/components/inputs/TextInput.svelte | 2 +- src/lib/modules/crypto.ts | 66 +++++++++++-- src/lib/services/key.ts | 11 ++- src/lib/stores/key.ts | 3 +- .../(fullscreen)/client/pending/+page.svelte | 2 +- .../(fullscreen)/key/generate/service.ts | 10 +- .../(main)/directory/[[id]]/+page.server.ts | 35 +++++++ .../(main)/directory/[[id]]/+page.svelte | 94 ++++++++++++++++++- .../directory/[[id]]/CreateBottomSheet.svelte | 32 +++++++ .../[[id]]/CreateDirectoryModal.svelte | 32 +++++++ .../directory/[[id]]/DirectoryEntry.svelte | 12 +++ src/routes/(main)/directory/[[id]]/service.ts | 49 ++++++++++ src/routes/services.ts | 2 +- 21 files changed, 403 insertions(+), 38 deletions(-) create mode 100644 src/lib/components/buttons/EntryButton.svelte create mode 100644 src/lib/components/buttons/FloatingButton.svelte create mode 100644 src/routes/(main)/directory/[[id]]/+page.server.ts create mode 100644 src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte create mode 100644 src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte create mode 100644 src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte create mode 100644 src/routes/(main)/directory/[[id]]/service.ts diff --git a/src/lib/components/BottomSheet.svelte b/src/lib/components/BottomSheet.svelte index 4accdf1..2bf18c5 100644 --- a/src/lib/components/BottomSheet.svelte +++ b/src/lib/components/BottomSheet.svelte @@ -18,10 +18,10 @@ onclick={() => { isOpen = false; }} - class="fixed inset-0 flex items-end justify-center" + class="fixed inset-0 z-10 flex items-end justify-center" >
-
+
e.stopPropagation()} diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 46c9e73..a83e4a1 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -5,25 +5,33 @@ interface Props { children: Snippet; + onClose?: () => void; isOpen: boolean; } - let { children, isOpen = $bindable() }: Props = $props(); + let { children, onClose, isOpen = $bindable() }: Props = $props(); + + const closeModal = $derived( + onClose || + (() => { + isOpen = false; + }), + ); {#if isOpen}
{ - isOpen = false; - }} - class="fixed inset-0 flex items-center justify-center bg-black bg-opacity-50 px-2" + onclick={closeModal} + class="fixed inset-0 z-10 bg-black bg-opacity-50 px-2" transition:fade={{ duration: 100 }} > -
e.stopPropagation()} class="max-w-full rounded-2xl bg-white p-4"> - {@render children?.()} +
+
e.stopPropagation()} class="max-w-full rounded-2xl bg-white p-4"> + {@render children?.()} +
diff --git a/src/lib/components/buttons/Button.svelte b/src/lib/components/buttons/Button.svelte index 484a2ab..692d4a1 100644 --- a/src/lib/components/buttons/Button.svelte +++ b/src/lib/components/buttons/Button.svelte @@ -9,13 +9,13 @@ let { children, color = "primary", onclick }: Props = $props(); - let bgColorStyle = $derived( + const bgColorStyle = $derived( { primary: "bg-primary-600 active:bg-primary-500", gray: "bg-gray-300 active:bg-gray-400", }[color], ); - let fontColorStyle = $derived( + const fontColorStyle = $derived( { primary: "text-white", gray: "text-gray-800", diff --git a/src/lib/components/buttons/EntryButton.svelte b/src/lib/components/buttons/EntryButton.svelte new file mode 100644 index 0000000..b370252 --- /dev/null +++ b/src/lib/components/buttons/EntryButton.svelte @@ -0,0 +1,30 @@ + + + diff --git a/src/lib/components/buttons/FloatingButton.svelte b/src/lib/components/buttons/FloatingButton.svelte new file mode 100644 index 0000000..823af8d --- /dev/null +++ b/src/lib/components/buttons/FloatingButton.svelte @@ -0,0 +1,22 @@ + + + diff --git a/src/lib/components/buttons/index.ts b/src/lib/components/buttons/index.ts index 0b93571..1765400 100644 --- a/src/lib/components/buttons/index.ts +++ b/src/lib/components/buttons/index.ts @@ -1,2 +1,4 @@ export { default as Button } from "./Button.svelte"; +export { default as EntryButton } from "./EntryButton.svelte"; +export { default as FloatingButton } from "./FloatingButton.svelte"; export { default as TextButton } from "./TextButton.svelte"; diff --git a/src/lib/components/divs/AdaptiveDiv.svelte b/src/lib/components/divs/AdaptiveDiv.svelte index 4e15d6b..ee845cc 100644 --- a/src/lib/components/divs/AdaptiveDiv.svelte +++ b/src/lib/components/divs/AdaptiveDiv.svelte @@ -2,6 +2,6 @@ let { children } = $props(); -
+
{@render children?.()}
diff --git a/src/lib/components/divs/TitleDiv.svelte b/src/lib/components/divs/TitleDiv.svelte index fcf1141..3fba02e 100644 --- a/src/lib/components/divs/TitleDiv.svelte +++ b/src/lib/components/divs/TitleDiv.svelte @@ -7,13 +7,12 @@ children: Snippet; } - let { icon, children }: Props = $props(); + let { icon: Icon, children }: Props = $props();
- {#if icon} - {@const Icon = icon} + {#if Icon} {/if}
diff --git a/src/lib/components/inputs/TextInput.svelte b/src/lib/components/inputs/TextInput.svelte index 82b4182..77e3e95 100644 --- a/src/lib/components/inputs/TextInput.svelte +++ b/src/lib/components/inputs/TextInput.svelte @@ -8,7 +8,7 @@ let { placeholder, type = "text", value = $bindable("") }: Props = $props(); -
+
{ +export const generateAESMasterKey = async () => { + return await window.crypto.subtle.generateKey( + { + name: "AES-KW", + length: 256, + } satisfies AesKeyGenParams, + true, + ["wrapKey", "unwrapKey"], + ); +}; + +export const generateAESDataKey = async () => { return await window.crypto.subtle.generateKey( { name: "AES-GCM", @@ -117,13 +128,45 @@ export const exportAESKey = async (key: CryptoKey) => { return await window.crypto.subtle.exportKey("raw", key); }; -export const wrapAESKeyUsingRSA = async (aesKey: CryptoKey, rsaPublicKey: CryptoKey) => { +export const encryptAESPlaintext = async (plaintext: BufferSource, aesKey: CryptoKey) => { + const iv = window.crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + iv, + } satisfies AesGcmParams, + aesKey, + plaintext, + ); + return { ciphertext, iv }; +}; + +export const decryptAESCiphertext = async ( + ciphertext: BufferSource, + iv: BufferSource, + aesKey: CryptoKey, +) => { + return await window.crypto.subtle.decrypt( + { + name: "AES-GCM", + iv, + } satisfies AesGcmParams, + aesKey, + ciphertext, + ); +}; + +export const wrapAESMasterKey = async (aesKey: CryptoKey, rsaPublicKey: CryptoKey) => { return await window.crypto.subtle.wrapKey("raw", aesKey, rsaPublicKey, { name: "RSA-OAEP", } satisfies RsaOaepParams); }; -export const unwrapAESKeyUsingRSA = async (wrappedKey: BufferSource, rsaPrivateKey: CryptoKey) => { +export const wrapAESDataKey = async (aesKey: CryptoKey, aesWrapKey: CryptoKey) => { + return await window.crypto.subtle.wrapKey("raw", aesKey, aesWrapKey, "AES-KW"); +}; + +export const unwrapAESMasterKey = async (wrappedKey: BufferSource, rsaPrivateKey: CryptoKey) => { return await window.crypto.subtle.unwrapKey( "raw", wrappedKey, @@ -131,11 +174,20 @@ export const unwrapAESKeyUsingRSA = async (wrappedKey: BufferSource, rsaPrivateK { name: "RSA-OAEP", } satisfies RsaOaepParams, - { - name: "AES-GCM", - length: 256, - } satisfies AesKeyGenParams, + "AES-KW", true, + ["wrapKey", "unwrapKey"], + ); +}; + +export const unwrapAESDataKey = async (wrappedKey: BufferSource, aesMasterKey: CryptoKey) => { + return await window.crypto.subtle.unwrapKey( + "raw", + wrappedKey, + aesMasterKey, + "AES-KW", + "AES-GCM", + false, ["encrypt", "decrypt"], ); }; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index 2a38a5a..0d4b7a1 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -6,7 +6,7 @@ import { decryptRSACiphertext, signRSAMessage, makeAESKeyNonextractable, - unwrapAESKeyUsingRSA, + unwrapAESMasterKey, verifyMasterKeyWrappedSig, } from "$lib/modules/crypto"; import type { @@ -51,7 +51,7 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: version, state, masterKey: await makeAESKeyNonextractable( - await unwrapAESKeyUsingRSA(decodeFromBase64(masterKeyWrapped), decryptKey), + await unwrapAESMasterKey(decodeFromBase64(masterKeyWrapped), decryptKey), ), isValid: await verifyMasterKeyWrappedSig( version, @@ -68,7 +68,12 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: masterKeys.map(({ version, state, masterKey }) => ({ version, state, key: masterKey })), ); masterKeyStore.set( - new Map(masterKeys.map(({ version, state, masterKey }) => [version, { state, masterKey }])), + new Map( + masterKeys.map(({ version, state, masterKey }) => [ + version, + { version, state, key: masterKey }, + ]), + ), ); return true; diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts index 7690bbf..d742634 100644 --- a/src/lib/stores/key.ts +++ b/src/lib/stores/key.ts @@ -8,8 +8,9 @@ export interface ClientKeys { } export interface MasterKey { + version: number; state: "active" | "retired" | "dead"; - masterKey: CryptoKey; + key: CryptoKey; } export const clientKeyStore = writable(null); diff --git a/src/routes/(fullscreen)/client/pending/+page.svelte b/src/routes/(fullscreen)/client/pending/+page.svelte index c5cfd17..ae5696d 100644 --- a/src/routes/(fullscreen)/client/pending/+page.svelte +++ b/src/routes/(fullscreen)/client/pending/+page.svelte @@ -8,7 +8,7 @@ let { data } = $props(); - let fingerprint = $derived( + const fingerprint = $derived( $clientKeyStore ? generateEncryptKeyFingerprint($clientKeyStore.encryptKey) : undefined, ); diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index 77c3f6f..b8a4a9f 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -2,9 +2,8 @@ import { generateRSAKeyPair, makeRSAKeyNonextractable, exportRSAKeyToBase64, - generateAESKey, - makeAESKeyNonextractable, - wrapAESKeyUsingRSA, + generateAESMasterKey, + wrapAESMasterKey, } from "$lib/modules/crypto"; import { clientKeyStore } from "$lib/stores"; @@ -29,9 +28,8 @@ export const generateClientKeys = async () => { }; export const generateInitialMasterKey = async (encryptKey: CryptoKey) => { - const masterKey = await generateAESKey(); + const masterKey = await generateAESMasterKey(); return { - masterKey: await makeAESKeyNonextractable(masterKey), - masterKeyWrapped: await wrapAESKeyUsingRSA(masterKey, encryptKey), + masterKeyWrapped: await wrapAESMasterKey(masterKey, encryptKey), }; }; diff --git a/src/routes/(main)/directory/[[id]]/+page.server.ts b/src/routes/(main)/directory/[[id]]/+page.server.ts new file mode 100644 index 0000000..2da0522 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/+page.server.ts @@ -0,0 +1,35 @@ +import { error } from "@sveltejs/kit"; +import { z } from "zod"; +import type { DirectroyInfoResponse } from "$lib/server/schemas"; +import type { PageServerLoad } from "./$types"; + +export const load: PageServerLoad = async ({ params, fetch }) => { + const zodRes = z + .object({ + id: z.coerce.number().int().positive().optional(), + }) + .safeParse(params); + if (!zodRes.success) error(404, "Not found"); + const { id } = zodRes.data; + + const directoryId = id ? id : ("root" as const); + const res = await fetch(`/api/directory/${directoryId}`); + if (!res.ok) error(404, "Not found"); + + const directoryInfo: DirectroyInfoResponse = await res.json(); + const subDirectoryInfos = await Promise.all( + directoryInfo.subDirectories.map(async (subDirectoryId) => { + const res = await fetch(`/api/directory/${subDirectoryId}`); + if (!res.ok) error(500, "Internal server error"); + return (await res.json()) as DirectroyInfoResponse; + }), + ); + const fileInfos = directoryInfo.files; // TODO + + return { + id: directoryId, + metadata: directoryInfo.metadata, + subDirectories: subDirectoryInfos, + files: fileInfos, + }; +}; diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 0f34ad6..518c8d2 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,3 +1,91 @@ -{#each Array(300) as _} -

Hello!

-{/each} + + + + 파일 + + +
+
+ {#if entries} + {#await entries then entries} + {#each entries as { name }} + + {/each} + {/await} + {/if} +
+ + { + isCreateBottomSheetOpen = true; + }} + /> +
+ + { + isCreateBottomSheetOpen = false; + isCreateDirectoryModalOpen = true; + }} + onFileUpload={() => { + isCreateBottomSheetOpen = false; + // TODO + }} +/> + diff --git a/src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte b/src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte new file mode 100644 index 0000000..9ea26ce --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/CreateBottomSheet.svelte @@ -0,0 +1,32 @@ + + + +
+ +
+ +

폴더 만들기

+
+
+ +
+ +

파일 업로드

+
+
+
+
diff --git a/src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte b/src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte new file mode 100644 index 0000000..4828b87 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/CreateDirectoryModal.svelte @@ -0,0 +1,32 @@ + + + +
+

새 폴더

+
+ +
+
+ + +
+
+
diff --git a/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte new file mode 100644 index 0000000..ceb3460 --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/DirectoryEntry.svelte @@ -0,0 +1,12 @@ + + +
+ +

{name}

+
diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts new file mode 100644 index 0000000..782197e --- /dev/null +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -0,0 +1,49 @@ +import { callSignedPostApi } from "$lib/hooks"; +import { + encodeToBase64, + decodeFromBase64, + generateAESDataKey, + encryptAESPlaintext, + decryptAESCiphertext, + wrapAESDataKey, + unwrapAESDataKey, +} from "$lib/modules/crypto"; +import type { DirectroyInfoResponse, DirectoryCreateRequest } from "$lib/server/schemas"; +import type { MasterKey } from "$lib/stores"; + +export const decryptDirectroyMetadata = async ( + metadata: NonNullable, + masterKey: CryptoKey, +) => { + const dataDecryptKey = await unwrapAESDataKey(decodeFromBase64(metadata.dek), masterKey); + return { + name: new TextDecoder().decode( + await decryptAESCiphertext( + decodeFromBase64(metadata.name), + decodeFromBase64(metadata.nameIv), + dataDecryptKey, + ), + ), + }; +}; + +export const requestDirectroyCreation = async ( + name: string, + parentId: "root" | number, + masterKey: MasterKey, + signKey: CryptoKey, +) => { + const dataKey = await generateAESDataKey(); + const nameEncrypted = await encryptAESPlaintext(new TextEncoder().encode(name), dataKey); + return await callSignedPostApi( + "/api/directory/create", + { + parentId, + mekVersion: masterKey.version, + dek: encodeToBase64(await wrapAESDataKey(dataKey, masterKey.key)), + name: encodeToBase64(nameEncrypted.ciphertext), + nameIv: encodeToBase64(nameEncrypted.iv.buffer), + }, + signKey, + ); +}; diff --git a/src/routes/services.ts b/src/routes/services.ts index 1b5c854..de8f618 100644 --- a/src/routes/services.ts +++ b/src/routes/services.ts @@ -18,7 +18,7 @@ export const prepareMasterKeyStore = async () => { const masterKeys = await getMasterKeys(); if (masterKeys.length > 0) { masterKeyStore.set( - new Map(masterKeys.map(({ version, state, key }) => [version, { state, masterKey: key }])), + new Map(masterKeys.map(({ version, state, key }) => [version, { version, state, key }])), ); return true; } else {