파일 다운로드 임시 구현

This commit is contained in:
static
2025-01-05 20:45:31 +09:00
parent 9ca6444bc9
commit c580556740
7 changed files with 137 additions and 18 deletions

View File

@@ -1,4 +1,4 @@
import { encodeToBase64, decodeFromBase64 } from "./util";
import { encodeString, decodeString, encodeToBase64, decodeFromBase64 } from "./util";
export const generateMasterKey = async () => {
return {
@@ -81,3 +81,12 @@ export const decryptData = async (ciphertext: BufferSource, iv: string, dataKey:
ciphertext,
);
};
export const encryptString = async (plaintext: string, dataKey: CryptoKey) => {
const { ciphertext, iv } = await encryptData(encodeString(plaintext), dataKey);
return { ciphertext: encodeToBase64(ciphertext), iv };
};
export const decryptString = async (ciphertext: string, iv: string, dataKey: CryptoKey) => {
return decodeString(await decryptData(decodeFromBase64(ciphertext), iv, dataKey));
};

View File

@@ -1,3 +1,14 @@
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
export const encodeString = (data: string) => {
return textEncoder.encode(data);
};
export const decodeString = (data: ArrayBuffer) => {
return textDecoder.decode(data);
};
export const encodeToBase64 = (data: ArrayBuffer) => {
return btoa(String.fromCharCode(...new Uint8Array(data)));
};

10
src/lib/services/file.ts Normal file
View File

@@ -0,0 +1,10 @@
import { unwrapDataKey, decryptString } from "$lib/modules/crypto";
import type { FileInfoResponse } from "$lib/server/schemas";
export const decryptFileMetadata = async (metadata: FileInfoResponse, masterKey: CryptoKey) => {
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
return {
dataKey,
name: await decryptString(metadata.name, metadata.nameIv, dataKey),
};
};

View File

@@ -0,0 +1,42 @@
<script lang="ts">
import FileSaver from "file-saver";
import { TopBar } from "$lib/components";
import { masterKeyStore } from "$lib/stores";
import { decryptFileMetadata, requestFileDownload } from "./service";
let { data } = $props();
let metadata = $state<Awaited<ReturnType<typeof decryptFileMetadata>> | undefined>();
$effect(() => {
if ($masterKeyStore) {
decryptFileMetadata(data.metadata, $masterKeyStore.get(data.metadata.mekVersion)!.key).then(
async (_metadata) => {
metadata = _metadata;
const file = await requestFileDownload(
data.id,
data.metadata.contentIv,
_metadata.dataKey,
);
// TODO: Preview
const blob = new Blob([file]);
FileSaver.saveAs(blob, metadata.name);
},
);
}
});
</script>
<svelte:head>
<title>파일</title>
</svelte:head>
{#if metadata}
<TopBar title={metadata.name} />
{:else}
<TopBar />
{/if}

View File

@@ -0,0 +1,24 @@
import { error } from "@sveltejs/kit";
import { z } from "zod";
import { callGetApi } from "$lib/hooks";
import type { FileInfoResponse } from "$lib/server/schemas";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ params, fetch }) => {
const zodRes = z
.object({
id: z.coerce.number().int().positive(),
})
.safeParse(params);
if (!zodRes.success) error(404, "Not found");
const { id } = zodRes.data;
const res = await callGetApi(`/api/file/${id}`, fetch);
if (!res.ok) error(404, "Not found");
const fileInfo: FileInfoResponse = await res.json();
return {
id,
metadata: fileInfo,
};
};

View File

@@ -0,0 +1,33 @@
import { decryptData } from "$lib/modules/crypto";
export { decryptFileMetadata } from "$lib/services/file";
export const requestFileDownload = (
fileId: number,
fileEncryptedIv: string,
dataKey: CryptoKey,
) => {
return new Promise<ArrayBuffer>((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.responseType = "arraybuffer";
xhr.addEventListener("load", async () => {
if (xhr.status !== 200) {
reject(new Error("Failed to download file"));
return;
}
const fileDecrypted = await decryptData(
xhr.response as ArrayBuffer,
fileEncryptedIv,
dataKey,
);
resolve(fileDecrypted);
});
// TODO: Progress, ...
xhr.open("GET", `/api/file/${fileId}/download`);
xhr.send();
});
};

View File

@@ -1,12 +1,12 @@
import { callSignedPostApi } from "$lib/hooks";
import {
encodeToBase64,
decodeFromBase64,
generateDataKey,
wrapDataKey,
unwrapDataKey,
encryptData,
decryptData,
encryptString,
decryptString,
digestMessage,
signRequestBody,
} from "$lib/modules/crypto";
@@ -14,28 +14,18 @@ import type {
DirectroyInfoResponse,
DirectoryCreateRequest,
FileUploadRequest,
FileInfoResponse,
} from "$lib/server/schemas";
import type { MasterKey } from "$lib/stores";
export { decryptFileMetadata } from "$lib/services/file";
export const decryptDirectroyMetadata = async (
metadata: NonNullable<DirectroyInfoResponse["metadata"]>,
masterKey: CryptoKey,
) => {
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
return {
name: new TextDecoder().decode(
await decryptData(decodeFromBase64(metadata.name), metadata.nameIv, dataKey),
),
};
};
export const decryptFileMetadata = async (metadata: FileInfoResponse, masterKey: CryptoKey) => {
const { dataKey } = await unwrapDataKey(metadata.dek, masterKey);
return {
name: new TextDecoder().decode(
await decryptData(decodeFromBase64(metadata.name), metadata.nameIv, dataKey),
),
name: await decryptString(metadata.name, metadata.nameIv, dataKey),
};
};
@@ -69,7 +59,7 @@ export const requestFileUpload = async (
const { dataKey } = await generateDataKey();
const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey);
const fileEncryptedHash = await digestMessage(fileEncrypted.ciphertext);
const nameEncrypted = await encryptData(new TextEncoder().encode(file.name), dataKey);
const nameEncrypted = await encryptString(file.name, dataKey);
const form = new FormData();
form.set(
@@ -81,7 +71,7 @@ export const requestFileUpload = async (
dek: await wrapDataKey(dataKey, masterKey.key),
contentHash: encodeToBase64(fileEncryptedHash),
contentIv: fileEncrypted.iv,
name: encodeToBase64(nameEncrypted.ciphertext),
name: nameEncrypted.ciphertext,
nameIv: nameEncrypted.iv,
},
signKey,