From 6bf40e4ab46c52e4e9df75927e7408b1e4203354 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 6 Jan 2025 03:47:33 +0900 Subject: [PATCH] =?UTF-8?q?Request=20=EC=84=9C=EB=AA=85=20=EC=8B=9C?= =?UTF-8?q?=EC=8A=A4=ED=85=9C=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 보안에 큰 도움이 되지 않는다고 판단하여 삭제하였습니다. 판단 근거는 다음과 같습니다. 1. Web Crypto API는 HTTPS 환경에서만 사용할 수 있음 2. 프론트엔드와 백엔드가 하나의 서버에서 제공되므로, 리버스 프록시에 의한 중간자 공격을 받지 않는가에 대한 직관적인 검증이 불가능함 3. 신뢰할 수 없는 리버스 프록시는 애초에 사용하지 않는 것이 맞음 다만 MEK에 대한 서명 등은 그대로 유지됩니다. --- src/lib/hooks/callApi.ts | 19 -------- src/lib/modules/crypto/rsa.ts | 9 ---- src/lib/server/modules/crypto.ts | 31 ------------- src/lib/server/schemas/file.ts | 1 - src/lib/server/services/file.ts | 26 +---------- src/routes/(fullscreen)/key/export/service.ts | 14 +++--- .../(main)/directory/[[id]]/+page.svelte | 11 ++--- src/routes/(main)/directory/[[id]]/service.ts | 45 +++++++------------ .../api/directory/[id]/rename/+server.ts | 18 ++++---- src/routes/api/directory/create/+server.ts | 14 +++--- src/routes/api/file/[id]/rename/+server.ts | 18 ++++---- src/routes/api/file/upload/+server.ts | 17 +++---- .../api/mek/register/initial/+server.ts | 9 ++-- 13 files changed, 57 insertions(+), 175 deletions(-) diff --git a/src/lib/hooks/callApi.ts b/src/lib/hooks/callApi.ts index 933bbb0..d2987a9 100644 --- a/src/lib/hooks/callApi.ts +++ b/src/lib/hooks/callApi.ts @@ -1,5 +1,3 @@ -import { signRequestBody } from "$lib/modules/crypto"; - export const refreshToken = async (fetchInternal = fetch) => { return await fetchInternal("/api/auth/refreshToken", { method: "POST" }); }; @@ -35,20 +33,3 @@ export const callPostApi = async ( fetchInternal, ); }; - -export const callSignedPostApi = async ( - input: RequestInfo, - payload: T, - signKey: CryptoKey, - fetchInternal?: typeof fetch, -) => { - return await callApi( - input, - { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: await signRequestBody(payload, signKey), - }, - fetchInternal, - ); -}; diff --git a/src/lib/modules/crypto/rsa.ts b/src/lib/modules/crypto/rsa.ts index a82abee..c4a7be5 100644 --- a/src/lib/modules/crypto/rsa.ts +++ b/src/lib/modules/crypto/rsa.ts @@ -122,15 +122,6 @@ export const verifySignature = async ( ); }; -export const signRequestBody = async (requestBody: T, signKey: CryptoKey) => { - const dataBuffer = new TextEncoder().encode(JSON.stringify(requestBody)); - const signature = await signMessage(dataBuffer, signKey); - return JSON.stringify({ - data: requestBody, - signature: encodeToBase64(signature), - }); -}; - export const signMasterKeyWrapped = async ( masterKeyVersion: number, masterKeyWrapped: string, diff --git a/src/lib/server/modules/crypto.ts b/src/lib/server/modules/crypto.ts index 38efc7f..de3dbf4 100644 --- a/src/lib/server/modules/crypto.ts +++ b/src/lib/server/modules/crypto.ts @@ -1,8 +1,5 @@ -import { error } from "@sveltejs/kit"; import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto"; import { promisify } from "util"; -import { z } from "zod"; -import { getClient } from "$lib/server/db/client"; const makePubKeyToPem = (pubKey: string) => `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; @@ -37,31 +34,3 @@ export const generateChallenge = async (length: number, encPubKey: string) => { const challenge = encryptAsymmetric(answer, encPubKey); return { answer, challenge }; }; - -export const parseSignedRequest = async ( - clientId: number, - data: unknown, - schema: T, -) => { - const zodRes = z - .object({ - data: schema, - signature: z.string().base64().nonempty(), - }) - .safeParse(data); - if (!zodRes.success) error(400, "Invalid request body"); - - const { data: parsedData, signature } = zodRes.data; - if (!parsedData) error(500, "Invalid request body"); - - const client = await getClient(clientId); - if (!client) { - error(500, "Invalid access token"); - } else if ( - !verifySignature(Buffer.from(JSON.stringify(parsedData)), signature, client.sigPubKey) - ) { - error(400, "Invalid signature"); - } - - return parsedData; -}; diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index b753517..5c43f00 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -20,7 +20,6 @@ export const fileUploadRequest = z.object({ parentId: z.union([z.enum(["root"]), z.number().int().positive()]), mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), - contentHash: z.string().base64().nonempty(), contentIv: z.string().base64().nonempty(), name: z.string().base64().nonempty(), nameIv: z.string().base64().nonempty(), diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index e98a642..a884dac 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -1,5 +1,4 @@ import { error } from "@sveltejs/kit"; -import { createHash } from "crypto"; import { createReadStream, createWriteStream, ReadStream, WriteStream } from "fs"; import { mkdir, stat, unlink } from "fs/promises"; import { dirname } from "path"; @@ -106,7 +105,6 @@ const safeUnlink = async (path: string) => { export const uploadFile = async ( params: Omit, encContentStream: ReadableStream, - encContentHash: string, ) => { const activeMekVersion = await getActiveMekVersion(params.userId); if (activeMekVersion === null) { @@ -116,32 +114,12 @@ export const uploadFile = async ( } const path = `${env.libraryPath}/${params.userId}/${uuidv4()}`; - const hash = createHash("sha256"); - await mkdir(dirname(path), { recursive: true }); try { - const hashStream = new TransformStream({ - transform: (chunk, controller) => { - hash.update(chunk); - controller.enqueue(chunk); - }, - }); - const fileStream = convertToWritableStream( - createWriteStream(path, { flags: "wx", mode: 0o600 }), + await encContentStream.pipeTo( + convertToWritableStream(createWriteStream(path, { flags: "wx", mode: 0o600 })), ); - await encContentStream.pipeThrough(hashStream).pipeTo(fileStream); - } catch (e) { - await safeUnlink(path); - throw e; - } - - if (hash.digest("base64") !== encContentHash) { - await safeUnlink(path); - error(400, "Invalid content hash"); - } - - try { await registerNewFile({ ...params, path, diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index 415eb77..a96b4be 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,4 +1,4 @@ -import { callSignedPostApi } from "$lib/hooks"; +import { callPostApi } from "$lib/hooks"; import { storeClientKey } from "$lib/indexedDB"; import { signMasterKeyWrapped } from "$lib/modules/crypto"; import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas"; @@ -46,13 +46,9 @@ export const requestInitialMasterKeyRegistration = async ( masterKeyWrapped: string, signKey: CryptoKey, ) => { - const res = await callSignedPostApi( - "/api/mek/register/initial", - { - mek: masterKeyWrapped, - mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey), - }, - signKey, - ); + const res = await callPostApi("/api/mek/register/initial", { + mek: masterKeyWrapped, + mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey), + }); return res.ok || res.status === 409; }; diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index 51bab14..449c47b 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -10,7 +10,7 @@ import { goto } from "$app/navigation"; import { TopBar } from "$lib/components"; import { FloatingButton } from "$lib/components/buttons"; - import { clientKeyStore, masterKeyStore } from "$lib/stores"; + import { masterKeyStore } from "$lib/stores"; import CreateBottomSheet from "./CreateBottomSheet.svelte"; import CreateDirectoryModal from "./CreateDirectoryModal.svelte"; import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte"; @@ -81,12 +81,7 @@ }); const createDirectory = async (name: string) => { - await requestDirectroyCreation( - name, - data.id, - $masterKeyStore?.get(1)!, - $clientKeyStore?.signKey!, - ); + await requestDirectroyCreation(name, data.id, $masterKeyStore?.get(1)!); isCreateDirectoryModalOpen = false; }; @@ -94,7 +89,7 @@ const file = fileInput?.files?.[0]; if (!file) return; - requestFileUpload(file, data.id, $masterKeyStore?.get(1)!, $clientKeyStore?.signKey!); + requestFileUpload(file, data.id, $masterKeyStore?.get(1)!); }; diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts index d7cae06..4c17228 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -1,4 +1,4 @@ -import { callSignedPostApi } from "$lib/hooks"; +import { callPostApi } from "$lib/hooks"; import { encodeToBase64, generateDataKey, @@ -7,8 +7,6 @@ import { encryptData, encryptString, decryptString, - digestMessage, - signRequestBody, } from "$lib/modules/crypto"; import type { DirectroyInfoResponse, @@ -33,49 +31,38 @@ export const requestDirectroyCreation = async ( name: string, parentId: "root" | number, masterKey: MasterKey, - signKey: CryptoKey, ) => { const { dataKey } = await generateDataKey(); const nameEncrypted = await encryptData(new TextEncoder().encode(name), dataKey); - return await callSignedPostApi( - "/api/directory/create", - { - parentId, - mekVersion: masterKey.version, - dek: await wrapDataKey(dataKey, masterKey.key), - name: encodeToBase64(nameEncrypted.ciphertext), - nameIv: nameEncrypted.iv, - }, - signKey, - ); + return await callPostApi("/api/directory/create", { + parentId, + mekVersion: masterKey.version, + dek: await wrapDataKey(dataKey, masterKey.key), + name: encodeToBase64(nameEncrypted.ciphertext), + nameIv: nameEncrypted.iv, + }); }; export const requestFileUpload = async ( file: File, parentId: "root" | number, masterKey: MasterKey, - signKey: CryptoKey, ) => { const { dataKey } = await generateDataKey(); const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey); - const fileEncryptedHash = await digestMessage(fileEncrypted.ciphertext); const nameEncrypted = await encryptString(file.name, dataKey); const form = new FormData(); form.set( "metadata", - await signRequestBody( - { - parentId, - mekVersion: masterKey.version, - dek: await wrapDataKey(dataKey, masterKey.key), - contentHash: encodeToBase64(fileEncryptedHash), - contentIv: fileEncrypted.iv, - name: nameEncrypted.ciphertext, - nameIv: nameEncrypted.iv, - }, - signKey, - ), + JSON.stringify({ + parentId, + mekVersion: masterKey.version, + dek: await wrapDataKey(dataKey, masterKey.key), + contentIv: fileEncrypted.iv, + name: nameEncrypted.ciphertext, + nameIv: nameEncrypted.iv, + } satisfies FileUploadRequest), ); form.set("content", new Blob([fileEncrypted.ciphertext])); diff --git a/src/routes/api/directory/[id]/rename/+server.ts b/src/routes/api/directory/[id]/rename/+server.ts index 89fa98d..ee52ac5 100644 --- a/src/routes/api/directory/[id]/rename/+server.ts +++ b/src/routes/api/directory/[id]/rename/+server.ts @@ -1,26 +1,24 @@ import { error, text } from "@sveltejs/kit"; import { z } from "zod"; import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { directoryRenameRequest } from "$lib/server/schemas"; import { renameDirectory } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies, params }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); + const { userId } = await authorize(cookies, "activeClient"); - const zodRes = z + const paramsZodRes = z .object({ id: z.coerce.number().int().positive(), }) .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - const { name, nameIv } = await parseSignedRequest( - clientId, - await request.json(), - directoryRenameRequest, - ); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = directoryRenameRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { name, nameIv } = bodyZodRes.data; await renameDirectory(userId, id, name, nameIv); return text("Directory renamed", { headers: { "Content-Type": "text/plain" } }); diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts index 559d34b..0f97117 100644 --- a/src/routes/api/directory/create/+server.ts +++ b/src/routes/api/directory/create/+server.ts @@ -1,17 +1,15 @@ -import { text } from "@sveltejs/kit"; +import { error, text } from "@sveltejs/kit"; import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { directoryCreateRequest } from "$lib/server/schemas"; import { createDirectory } from "$lib/server/services/directory"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); - const { parentId, mekVersion, dek, name, nameIv } = await parseSignedRequest( - clientId, - await request.json(), - directoryCreateRequest, - ); + const { userId } = await authorize(cookies, "activeClient"); + + const zodRes = directoryCreateRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { parentId, mekVersion, dek, name, nameIv } = zodRes.data; await createDirectory({ userId, diff --git a/src/routes/api/file/[id]/rename/+server.ts b/src/routes/api/file/[id]/rename/+server.ts index 5c816a8..d9bcd60 100644 --- a/src/routes/api/file/[id]/rename/+server.ts +++ b/src/routes/api/file/[id]/rename/+server.ts @@ -1,26 +1,24 @@ import { error, text } from "@sveltejs/kit"; import { z } from "zod"; import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { fileRenameRequest } from "$lib/server/schemas"; import { renameFile } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies, params }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); + const { userId } = await authorize(cookies, "activeClient"); - const zodRes = z + const paramsZodRes = z .object({ id: z.coerce.number().int().positive(), }) .safeParse(params); - if (!zodRes.success) error(400, "Invalid path parameters"); - const { id } = zodRes.data; - const { name, nameIv } = await parseSignedRequest( - clientId, - await request.json(), - fileRenameRequest, - ); + if (!paramsZodRes.success) error(400, "Invalid path parameters"); + const { id } = paramsZodRes.data; + + const bodyZodRes = fileRenameRequest.safeParse(await request.json()); + if (!bodyZodRes.success) error(400, "Invalid request body"); + const { name, nameIv } = bodyZodRes.data; await renameFile(userId, id, name, nameIv); return text("File renamed", { headers: { "Content-Type": "text/plain" } }); diff --git a/src/routes/api/file/upload/+server.ts b/src/routes/api/file/upload/+server.ts index 1cf9e87..0cd1cab 100644 --- a/src/routes/api/file/upload/+server.ts +++ b/src/routes/api/file/upload/+server.ts @@ -1,27 +1,23 @@ import { error, text } from "@sveltejs/kit"; import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { fileUploadRequest } from "$lib/server/schemas"; import { uploadFile } from "$lib/server/services/file"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ request, cookies }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); + const { userId } = await authorize(cookies, "activeClient"); const form = await request.formData(); - const metadata = form.get("metadata"); - if (!metadata || typeof metadata !== "string") { - error(400, "Invalid request body"); - } - const { parentId, mekVersion, dek, contentHash, contentIv, name, nameIv } = - await parseSignedRequest(clientId, JSON.parse(metadata), fileUploadRequest); - const content = form.get("content"); - if (!content || !(content instanceof File)) { + if (typeof metadata !== "string" || !(content instanceof File)) { error(400, "Invalid request body"); } + const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata)); + if (!zodRes.success) error(400, "Invalid request body"); + const { parentId, mekVersion, dek, contentIv, name, nameIv } = zodRes.data; + await uploadFile( { userId, @@ -33,7 +29,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => { encNameIv: nameIv, }, content.stream(), - contentHash, ); return text("File uploaded", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/mek/register/initial/+server.ts b/src/routes/api/mek/register/initial/+server.ts index c39ef37..ba959fb 100644 --- a/src/routes/api/mek/register/initial/+server.ts +++ b/src/routes/api/mek/register/initial/+server.ts @@ -1,6 +1,5 @@ import { error, text } from "@sveltejs/kit"; import { authenticate } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; import { initialMasterKeyRegisterRequest } from "$lib/server/schemas"; import { registerInitialActiveMek } from "$lib/server/services/mek"; import type { RequestHandler } from "./$types"; @@ -11,11 +10,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => { error(403, "Forbidden"); } - const { mek, mekSig } = await parseSignedRequest( - clientId, - await request.json(), - initialMasterKeyRegisterRequest, - ); + const zodRes = initialMasterKeyRegisterRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { mek, mekSig } = zodRes.data; await registerInitialActiveMek(userId, clientId, mek, mekSig); return text("MEK registered", { headers: { "Content-Type": "text/plain" } });