Request 서명 시스템 삭제

보안에 큰 도움이 되지 않는다고 판단하여 삭제하였습니다. 판단 근거는 다음과 같습니다.

1. Web Crypto API는 HTTPS 환경에서만 사용할 수 있음
2. 프론트엔드와 백엔드가 하나의 서버에서 제공되므로, 리버스 프록시에 의한 중간자 공격을 받지 않는가에 대한 직관적인 검증이 불가능함
3. 신뢰할 수 없는 리버스 프록시는 애초에 사용하지 않는 것이 맞음

다만 MEK에 대한 서명 등은 그대로 유지됩니다.
This commit is contained in:
static
2025-01-06 03:47:33 +09:00
parent 9fad26d538
commit 6bf40e4ab4
13 changed files with 57 additions and 175 deletions

View File

@@ -1,5 +1,3 @@
import { signRequestBody } from "$lib/modules/crypto";
export const refreshToken = async (fetchInternal = fetch) => { export const refreshToken = async (fetchInternal = fetch) => {
return await fetchInternal("/api/auth/refreshToken", { method: "POST" }); return await fetchInternal("/api/auth/refreshToken", { method: "POST" });
}; };
@@ -35,20 +33,3 @@ export const callPostApi = async <T>(
fetchInternal, fetchInternal,
); );
}; };
export const callSignedPostApi = async <T>(
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,
);
};

View File

@@ -122,15 +122,6 @@ export const verifySignature = async (
); );
}; };
export const signRequestBody = async <T>(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 ( export const signMasterKeyWrapped = async (
masterKeyVersion: number, masterKeyVersion: number,
masterKeyWrapped: string, masterKeyWrapped: string,

View File

@@ -1,8 +1,5 @@
import { error } from "@sveltejs/kit";
import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto"; import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto";
import { promisify } from "util"; import { promisify } from "util";
import { z } from "zod";
import { getClient } from "$lib/server/db/client";
const makePubKeyToPem = (pubKey: string) => const makePubKeyToPem = (pubKey: string) =>
`-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; `-----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); const challenge = encryptAsymmetric(answer, encPubKey);
return { answer, challenge }; return { answer, challenge };
}; };
export const parseSignedRequest = async <T extends z.ZodTypeAny>(
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;
};

View File

@@ -20,7 +20,6 @@ export const fileUploadRequest = z.object({
parentId: z.union([z.enum(["root"]), z.number().int().positive()]), parentId: z.union([z.enum(["root"]), z.number().int().positive()]),
mekVersion: z.number().int().positive(), mekVersion: z.number().int().positive(),
dek: z.string().base64().nonempty(), dek: z.string().base64().nonempty(),
contentHash: z.string().base64().nonempty(),
contentIv: z.string().base64().nonempty(), contentIv: z.string().base64().nonempty(),
name: z.string().base64().nonempty(), name: z.string().base64().nonempty(),
nameIv: z.string().base64().nonempty(), nameIv: z.string().base64().nonempty(),

View File

@@ -1,5 +1,4 @@
import { error } from "@sveltejs/kit"; import { error } from "@sveltejs/kit";
import { createHash } from "crypto";
import { createReadStream, createWriteStream, ReadStream, WriteStream } from "fs"; import { createReadStream, createWriteStream, ReadStream, WriteStream } from "fs";
import { mkdir, stat, unlink } from "fs/promises"; import { mkdir, stat, unlink } from "fs/promises";
import { dirname } from "path"; import { dirname } from "path";
@@ -106,7 +105,6 @@ const safeUnlink = async (path: string) => {
export const uploadFile = async ( export const uploadFile = async (
params: Omit<NewFileParams, "path">, params: Omit<NewFileParams, "path">,
encContentStream: ReadableStream<Uint8Array>, encContentStream: ReadableStream<Uint8Array>,
encContentHash: string,
) => { ) => {
const activeMekVersion = await getActiveMekVersion(params.userId); const activeMekVersion = await getActiveMekVersion(params.userId);
if (activeMekVersion === null) { if (activeMekVersion === null) {
@@ -116,32 +114,12 @@ export const uploadFile = async (
} }
const path = `${env.libraryPath}/${params.userId}/${uuidv4()}`; const path = `${env.libraryPath}/${params.userId}/${uuidv4()}`;
const hash = createHash("sha256");
await mkdir(dirname(path), { recursive: true }); await mkdir(dirname(path), { recursive: true });
try { try {
const hashStream = new TransformStream<Uint8Array, Uint8Array>({ await encContentStream.pipeTo(
transform: (chunk, controller) => { convertToWritableStream(createWriteStream(path, { flags: "wx", mode: 0o600 })),
hash.update(chunk);
controller.enqueue(chunk);
},
});
const fileStream = 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({ await registerNewFile({
...params, ...params,
path, path,

View File

@@ -1,4 +1,4 @@
import { callSignedPostApi } from "$lib/hooks"; import { callPostApi } from "$lib/hooks";
import { storeClientKey } from "$lib/indexedDB"; import { storeClientKey } from "$lib/indexedDB";
import { signMasterKeyWrapped } from "$lib/modules/crypto"; import { signMasterKeyWrapped } from "$lib/modules/crypto";
import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas"; import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas";
@@ -46,13 +46,9 @@ export const requestInitialMasterKeyRegistration = async (
masterKeyWrapped: string, masterKeyWrapped: string,
signKey: CryptoKey, signKey: CryptoKey,
) => { ) => {
const res = await callSignedPostApi<InitialMasterKeyRegisterRequest>( const res = await callPostApi<InitialMasterKeyRegisterRequest>("/api/mek/register/initial", {
"/api/mek/register/initial", mek: masterKeyWrapped,
{ mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey),
mek: masterKeyWrapped, });
mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey),
},
signKey,
);
return res.ok || res.status === 409; return res.ok || res.status === 409;
}; };

View File

@@ -10,7 +10,7 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { TopBar } from "$lib/components"; import { TopBar } from "$lib/components";
import { FloatingButton } from "$lib/components/buttons"; import { FloatingButton } from "$lib/components/buttons";
import { clientKeyStore, masterKeyStore } from "$lib/stores"; import { masterKeyStore } from "$lib/stores";
import CreateBottomSheet from "./CreateBottomSheet.svelte"; import CreateBottomSheet from "./CreateBottomSheet.svelte";
import CreateDirectoryModal from "./CreateDirectoryModal.svelte"; import CreateDirectoryModal from "./CreateDirectoryModal.svelte";
import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte"; import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte";
@@ -81,12 +81,7 @@
}); });
const createDirectory = async (name: string) => { const createDirectory = async (name: string) => {
await requestDirectroyCreation( await requestDirectroyCreation(name, data.id, $masterKeyStore?.get(1)!);
name,
data.id,
$masterKeyStore?.get(1)!,
$clientKeyStore?.signKey!,
);
isCreateDirectoryModalOpen = false; isCreateDirectoryModalOpen = false;
}; };
@@ -94,7 +89,7 @@
const file = fileInput?.files?.[0]; const file = fileInput?.files?.[0];
if (!file) return; if (!file) return;
requestFileUpload(file, data.id, $masterKeyStore?.get(1)!, $clientKeyStore?.signKey!); requestFileUpload(file, data.id, $masterKeyStore?.get(1)!);
}; };
</script> </script>

View File

@@ -1,4 +1,4 @@
import { callSignedPostApi } from "$lib/hooks"; import { callPostApi } from "$lib/hooks";
import { import {
encodeToBase64, encodeToBase64,
generateDataKey, generateDataKey,
@@ -7,8 +7,6 @@ import {
encryptData, encryptData,
encryptString, encryptString,
decryptString, decryptString,
digestMessage,
signRequestBody,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
import type { import type {
DirectroyInfoResponse, DirectroyInfoResponse,
@@ -33,49 +31,38 @@ export const requestDirectroyCreation = async (
name: string, name: string,
parentId: "root" | number, parentId: "root" | number,
masterKey: MasterKey, masterKey: MasterKey,
signKey: CryptoKey,
) => { ) => {
const { dataKey } = await generateDataKey(); const { dataKey } = await generateDataKey();
const nameEncrypted = await encryptData(new TextEncoder().encode(name), dataKey); const nameEncrypted = await encryptData(new TextEncoder().encode(name), dataKey);
return await callSignedPostApi<DirectoryCreateRequest>( return await callPostApi<DirectoryCreateRequest>("/api/directory/create", {
"/api/directory/create", parentId,
{ mekVersion: masterKey.version,
parentId, dek: await wrapDataKey(dataKey, masterKey.key),
mekVersion: masterKey.version, name: encodeToBase64(nameEncrypted.ciphertext),
dek: await wrapDataKey(dataKey, masterKey.key), nameIv: nameEncrypted.iv,
name: encodeToBase64(nameEncrypted.ciphertext), });
nameIv: nameEncrypted.iv,
},
signKey,
);
}; };
export const requestFileUpload = async ( export const requestFileUpload = async (
file: File, file: File,
parentId: "root" | number, parentId: "root" | number,
masterKey: MasterKey, masterKey: MasterKey,
signKey: CryptoKey,
) => { ) => {
const { dataKey } = await generateDataKey(); const { dataKey } = await generateDataKey();
const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey); const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey);
const fileEncryptedHash = await digestMessage(fileEncrypted.ciphertext);
const nameEncrypted = await encryptString(file.name, dataKey); const nameEncrypted = await encryptString(file.name, dataKey);
const form = new FormData(); const form = new FormData();
form.set( form.set(
"metadata", "metadata",
await signRequestBody<FileUploadRequest>( JSON.stringify({
{ parentId,
parentId, mekVersion: masterKey.version,
mekVersion: masterKey.version, dek: await wrapDataKey(dataKey, masterKey.key),
dek: await wrapDataKey(dataKey, masterKey.key), contentIv: fileEncrypted.iv,
contentHash: encodeToBase64(fileEncryptedHash), name: nameEncrypted.ciphertext,
contentIv: fileEncrypted.iv, nameIv: nameEncrypted.iv,
name: nameEncrypted.ciphertext, } satisfies FileUploadRequest),
nameIv: nameEncrypted.iv,
},
signKey,
),
); );
form.set("content", new Blob([fileEncrypted.ciphertext])); form.set("content", new Blob([fileEncrypted.ciphertext]));

View File

@@ -1,26 +1,24 @@
import { error, text } from "@sveltejs/kit"; import { error, text } from "@sveltejs/kit";
import { z } from "zod"; import { z } from "zod";
import { authorize } from "$lib/server/modules/auth"; import { authorize } from "$lib/server/modules/auth";
import { parseSignedRequest } from "$lib/server/modules/crypto";
import { directoryRenameRequest } from "$lib/server/schemas"; import { directoryRenameRequest } from "$lib/server/schemas";
import { renameDirectory } from "$lib/server/services/directory"; import { renameDirectory } from "$lib/server/services/directory";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request, cookies, params }) => { 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({ .object({
id: z.coerce.number().int().positive(), id: z.coerce.number().int().positive(),
}) })
.safeParse(params); .safeParse(params);
if (!zodRes.success) error(400, "Invalid path parameters"); if (!paramsZodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data; const { id } = paramsZodRes.data;
const { name, nameIv } = await parseSignedRequest(
clientId, const bodyZodRes = directoryRenameRequest.safeParse(await request.json());
await request.json(), if (!bodyZodRes.success) error(400, "Invalid request body");
directoryRenameRequest, const { name, nameIv } = bodyZodRes.data;
);
await renameDirectory(userId, id, name, nameIv); await renameDirectory(userId, id, name, nameIv);
return text("Directory renamed", { headers: { "Content-Type": "text/plain" } }); return text("Directory renamed", { headers: { "Content-Type": "text/plain" } });

View File

@@ -1,17 +1,15 @@
import { text } from "@sveltejs/kit"; import { error, text } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth"; import { authorize } from "$lib/server/modules/auth";
import { parseSignedRequest } from "$lib/server/modules/crypto";
import { directoryCreateRequest } from "$lib/server/schemas"; import { directoryCreateRequest } from "$lib/server/schemas";
import { createDirectory } from "$lib/server/services/directory"; import { createDirectory } from "$lib/server/services/directory";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request, cookies }) => { export const POST: RequestHandler = async ({ request, cookies }) => {
const { userId, clientId } = await authorize(cookies, "activeClient"); const { userId } = await authorize(cookies, "activeClient");
const { parentId, mekVersion, dek, name, nameIv } = await parseSignedRequest(
clientId, const zodRes = directoryCreateRequest.safeParse(await request.json());
await request.json(), if (!zodRes.success) error(400, "Invalid request body");
directoryCreateRequest, const { parentId, mekVersion, dek, name, nameIv } = zodRes.data;
);
await createDirectory({ await createDirectory({
userId, userId,

View File

@@ -1,26 +1,24 @@
import { error, text } from "@sveltejs/kit"; import { error, text } from "@sveltejs/kit";
import { z } from "zod"; import { z } from "zod";
import { authorize } from "$lib/server/modules/auth"; import { authorize } from "$lib/server/modules/auth";
import { parseSignedRequest } from "$lib/server/modules/crypto";
import { fileRenameRequest } from "$lib/server/schemas"; import { fileRenameRequest } from "$lib/server/schemas";
import { renameFile } from "$lib/server/services/file"; import { renameFile } from "$lib/server/services/file";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request, cookies, params }) => { 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({ .object({
id: z.coerce.number().int().positive(), id: z.coerce.number().int().positive(),
}) })
.safeParse(params); .safeParse(params);
if (!zodRes.success) error(400, "Invalid path parameters"); if (!paramsZodRes.success) error(400, "Invalid path parameters");
const { id } = zodRes.data; const { id } = paramsZodRes.data;
const { name, nameIv } = await parseSignedRequest(
clientId, const bodyZodRes = fileRenameRequest.safeParse(await request.json());
await request.json(), if (!bodyZodRes.success) error(400, "Invalid request body");
fileRenameRequest, const { name, nameIv } = bodyZodRes.data;
);
await renameFile(userId, id, name, nameIv); await renameFile(userId, id, name, nameIv);
return text("File renamed", { headers: { "Content-Type": "text/plain" } }); return text("File renamed", { headers: { "Content-Type": "text/plain" } });

View File

@@ -1,27 +1,23 @@
import { error, text } from "@sveltejs/kit"; import { error, text } from "@sveltejs/kit";
import { authorize } from "$lib/server/modules/auth"; import { authorize } from "$lib/server/modules/auth";
import { parseSignedRequest } from "$lib/server/modules/crypto";
import { fileUploadRequest } from "$lib/server/schemas"; import { fileUploadRequest } from "$lib/server/schemas";
import { uploadFile } from "$lib/server/services/file"; import { uploadFile } from "$lib/server/services/file";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
export const POST: RequestHandler = async ({ request, cookies }) => { 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 form = await request.formData();
const metadata = form.get("metadata"); 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"); const content = form.get("content");
if (!content || !(content instanceof File)) { if (typeof metadata !== "string" || !(content instanceof File)) {
error(400, "Invalid request body"); 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( await uploadFile(
{ {
userId, userId,
@@ -33,7 +29,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
encNameIv: nameIv, encNameIv: nameIv,
}, },
content.stream(), content.stream(),
contentHash,
); );
return text("File uploaded", { headers: { "Content-Type": "text/plain" } }); return text("File uploaded", { headers: { "Content-Type": "text/plain" } });
}; };

View File

@@ -1,6 +1,5 @@
import { error, text } from "@sveltejs/kit"; import { error, text } from "@sveltejs/kit";
import { authenticate } from "$lib/server/modules/auth"; import { authenticate } from "$lib/server/modules/auth";
import { parseSignedRequest } from "$lib/server/modules/crypto";
import { initialMasterKeyRegisterRequest } from "$lib/server/schemas"; import { initialMasterKeyRegisterRequest } from "$lib/server/schemas";
import { registerInitialActiveMek } from "$lib/server/services/mek"; import { registerInitialActiveMek } from "$lib/server/services/mek";
import type { RequestHandler } from "./$types"; import type { RequestHandler } from "./$types";
@@ -11,11 +10,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
error(403, "Forbidden"); error(403, "Forbidden");
} }
const { mek, mekSig } = await parseSignedRequest( const zodRes = initialMasterKeyRegisterRequest.safeParse(await request.json());
clientId, if (!zodRes.success) error(400, "Invalid request body");
await request.json(), const { mek, mekSig } = zodRes.data;
initialMasterKeyRegisterRequest,
);
await registerInitialActiveMek(userId, clientId, mek, mekSig); await registerInitialActiveMek(userId, clientId, mek, mekSig);
return text("MEK registered", { headers: { "Content-Type": "text/plain" } }); return text("MEK registered", { headers: { "Content-Type": "text/plain" } });