mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-16 06:58:46 +00:00
파일 업로드시의 체크섬 검사 구현
This commit is contained in:
1
drizzle/0002_good_talisman.sql
Normal file
1
drizzle/0002_good_talisman.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `file` ADD `encrypted_content_hash` text NOT NULL;
|
||||||
1308
drizzle/meta/0002_snapshot.json
Normal file
1308
drizzle/meta/0002_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,6 +15,13 @@
|
|||||||
"when": 1736720831242,
|
"when": 1736720831242,
|
||||||
"tag": "0001_blushing_alice",
|
"tag": "0001_blushing_alice",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1737191517463,
|
||||||
|
"tag": "0002_good_talisman",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
wrapDataKey,
|
wrapDataKey,
|
||||||
encryptData,
|
encryptData,
|
||||||
encryptString,
|
encryptString,
|
||||||
|
digestMessage,
|
||||||
signMessageHmac,
|
signMessageHmac,
|
||||||
} from "$lib/modules/crypto";
|
} from "$lib/modules/crypto";
|
||||||
import type {
|
import type {
|
||||||
@@ -97,6 +98,8 @@ const encryptFile = limitFunction(
|
|||||||
const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key);
|
const dataKeyWrapped = await wrapDataKey(dataKey, masterKey.key);
|
||||||
|
|
||||||
const fileEncrypted = await encryptData(fileBuffer, dataKey);
|
const fileEncrypted = await encryptData(fileBuffer, dataKey);
|
||||||
|
const fileEncryptedHash = encodeToBase64(await digestMessage(fileEncrypted.ciphertext));
|
||||||
|
|
||||||
const nameEncrypted = await encryptString(file.name, dataKey);
|
const nameEncrypted = await encryptString(file.name, dataKey);
|
||||||
const createdAtEncrypted =
|
const createdAtEncrypted =
|
||||||
createdAt && (await encryptString(createdAt.getTime().toString(), dataKey));
|
createdAt && (await encryptString(createdAt.getTime().toString(), dataKey));
|
||||||
@@ -110,8 +113,9 @@ const encryptFile = limitFunction(
|
|||||||
return {
|
return {
|
||||||
dataKeyWrapped,
|
dataKeyWrapped,
|
||||||
dataKeyVersion,
|
dataKeyVersion,
|
||||||
fileEncrypted,
|
|
||||||
fileType,
|
fileType,
|
||||||
|
fileEncrypted,
|
||||||
|
fileEncryptedHash,
|
||||||
nameEncrypted,
|
nameEncrypted,
|
||||||
createdAtEncrypted,
|
createdAtEncrypted,
|
||||||
lastModifiedAtEncrypted,
|
lastModifiedAtEncrypted,
|
||||||
@@ -184,8 +188,9 @@ export const uploadFile = async (
|
|||||||
const {
|
const {
|
||||||
dataKeyWrapped,
|
dataKeyWrapped,
|
||||||
dataKeyVersion,
|
dataKeyVersion,
|
||||||
fileEncrypted,
|
|
||||||
fileType,
|
fileType,
|
||||||
|
fileEncrypted,
|
||||||
|
fileEncryptedHash,
|
||||||
nameEncrypted,
|
nameEncrypted,
|
||||||
createdAtEncrypted,
|
createdAtEncrypted,
|
||||||
lastModifiedAtEncrypted,
|
lastModifiedAtEncrypted,
|
||||||
@@ -212,6 +217,7 @@ export const uploadFile = async (
|
|||||||
} as FileUploadRequest),
|
} as FileUploadRequest),
|
||||||
);
|
);
|
||||||
form.set("content", new Blob([fileEncrypted.ciphertext]));
|
form.set("content", new Blob([fileEncrypted.ciphertext]));
|
||||||
|
form.set("checksum", fileEncryptedHash);
|
||||||
|
|
||||||
await requestFileUpload(status, form);
|
await requestFileUpload(status, form);
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface NewFileParams {
|
|||||||
contentHmac: string | null;
|
contentHmac: string | null;
|
||||||
contentType: string;
|
contentType: string;
|
||||||
encContentIv: string;
|
encContentIv: string;
|
||||||
|
encContentHash: string;
|
||||||
encName: string;
|
encName: string;
|
||||||
encNameIv: string;
|
encNameIv: string;
|
||||||
encCreatedAt: string | null;
|
encCreatedAt: string | null;
|
||||||
@@ -198,11 +199,12 @@ export const registerFile = async (params: NewFileParams) => {
|
|||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
mekVersion: params.mekVersion,
|
mekVersion: params.mekVersion,
|
||||||
hskVersion: params.hskVersion,
|
hskVersion: params.hskVersion,
|
||||||
contentHmac: params.contentHmac,
|
|
||||||
contentType: params.contentType,
|
|
||||||
encDek: params.encDek,
|
encDek: params.encDek,
|
||||||
dekVersion: params.dekVersion,
|
dekVersion: params.dekVersion,
|
||||||
|
contentHmac: params.contentHmac,
|
||||||
|
contentType: params.contentType,
|
||||||
encContentIv: params.encContentIv,
|
encContentIv: params.encContentIv,
|
||||||
|
encContentHash: params.encContentHash,
|
||||||
encName: { ciphertext: params.encName, iv: params.encNameIv },
|
encName: { ciphertext: params.encName, iv: params.encNameIv },
|
||||||
encCreatedAt:
|
encCreatedAt:
|
||||||
params.encCreatedAt && params.encCreatedAtIv
|
params.encCreatedAt && params.encCreatedAtIv
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const file = sqliteTable(
|
|||||||
contentHmac: text("content_hmac"), // Base64
|
contentHmac: text("content_hmac"), // Base64
|
||||||
contentType: text("content_type").notNull(),
|
contentType: text("content_type").notNull(),
|
||||||
encContentIv: text("encrypted_content_iv").notNull(), // Base64
|
encContentIv: text("encrypted_content_iv").notNull(), // Base64
|
||||||
|
encContentHash: text("encrypted_content_hash").notNull(), // Base64
|
||||||
encName: ciphertext("encrypted_name").notNull(),
|
encName: ciphertext("encrypted_name").notNull(),
|
||||||
encCreatedAt: ciphertext("encrypted_created_at"),
|
encCreatedAt: ciphertext("encrypted_created_at"),
|
||||||
encLastModifiedAt: ciphertext("encrypted_last_modified_at").notNull(),
|
encLastModifiedAt: ciphertext("encrypted_last_modified_at").notNull(),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { error } from "@sveltejs/kit";
|
import { error } from "@sveltejs/kit";
|
||||||
|
import { createHash } from "crypto";
|
||||||
import { createReadStream, createWriteStream } from "fs";
|
import { createReadStream, createWriteStream } from "fs";
|
||||||
import { mkdir, stat, unlink } from "fs/promises";
|
import { mkdir, stat, unlink } from "fs/promises";
|
||||||
import { dirname } from "path";
|
import { dirname } from "path";
|
||||||
@@ -95,8 +96,9 @@ const safeUnlink = async (path: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const uploadFile = async (
|
export const uploadFile = async (
|
||||||
params: Omit<NewFileParams, "path">,
|
params: Omit<NewFileParams, "path" | "encContentHash">,
|
||||||
encContentStream: Readable,
|
encContentStream: Readable,
|
||||||
|
encContentHash: Promise<string>,
|
||||||
) => {
|
) => {
|
||||||
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
||||||
const oneMinuteLater = new Date(Date.now() + 60 * 1000);
|
const oneMinuteLater = new Date(Date.now() + 60 * 1000);
|
||||||
@@ -108,16 +110,30 @@ export const uploadFile = async (
|
|||||||
await mkdir(dirname(path), { recursive: true });
|
await mkdir(dirname(path), { recursive: true });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await pipeline(encContentStream, createWriteStream(path, { flags: "wx", mode: 0o600 }));
|
const hashStream = createHash("sha256");
|
||||||
|
const [_, hash] = await Promise.all([
|
||||||
|
pipeline(encContentStream, hashStream, createWriteStream(path, { flags: "wx", mode: 0o600 })),
|
||||||
|
encContentHash,
|
||||||
|
]);
|
||||||
|
if (hashStream.digest("base64") != hash) {
|
||||||
|
throw new Error("Invalid checksum");
|
||||||
|
}
|
||||||
|
|
||||||
await registerFile({
|
await registerFile({
|
||||||
...params,
|
...params,
|
||||||
path,
|
path,
|
||||||
|
encContentHash: hash,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await safeUnlink(path);
|
await safeUnlink(path);
|
||||||
|
|
||||||
if (e instanceof IntegrityError && e.message === "Inactive MEK version") {
|
if (e instanceof IntegrityError && e.message === "Inactive MEK version") {
|
||||||
error(400, "Invalid MEK version");
|
error(400, "Invalid MEK version");
|
||||||
|
} else if (
|
||||||
|
e instanceof Error &&
|
||||||
|
(e.message === "Invalid request body" || e.message === "Invalid checksum")
|
||||||
|
) {
|
||||||
|
error(400, "Invalid request body");
|
||||||
}
|
}
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,27 +67,39 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|||||||
|
|
||||||
let metadata: FileMetadata | null = null;
|
let metadata: FileMetadata | null = null;
|
||||||
let content: Readable | null = null;
|
let content: Readable | null = null;
|
||||||
|
const checksum = new Promise<string>((resolveChecksum, rejectChecksum) => {
|
||||||
|
bb.on(
|
||||||
|
"field",
|
||||||
|
handler(async (fieldname, val) => {
|
||||||
|
if (fieldname === "metadata") {
|
||||||
|
if (!metadata) {
|
||||||
|
// Ignore subsequent metadata fields
|
||||||
|
metadata = parseFileMetadata(userId, val);
|
||||||
|
}
|
||||||
|
} else if (fieldname === "checksum") {
|
||||||
|
resolveChecksum(val); // Ignore subsequent checksum fields
|
||||||
|
} else {
|
||||||
|
error(400, "Invalid request body");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
bb.on(
|
||||||
|
"file",
|
||||||
|
handler(async (fieldname, file) => {
|
||||||
|
if (fieldname !== "content") error(400, "Invalid request body");
|
||||||
|
if (!metadata || content) error(400, "Invalid request body");
|
||||||
|
content = file;
|
||||||
|
|
||||||
bb.on(
|
await uploadFile(metadata, content, checksum);
|
||||||
"field",
|
resolve(text("File uploaded", { headers: { "Content-Type": "text/plain" } }));
|
||||||
handler(async (fieldname, val) => {
|
}),
|
||||||
if (fieldname !== "metadata") error(400, "Invalid request body");
|
);
|
||||||
if (metadata || content) error(400, "Invalid request body");
|
bb.on("finish", () => rejectChecksum(new Error("Invalid request body")));
|
||||||
metadata = parseFileMetadata(userId, val);
|
bb.on("error", (e) => {
|
||||||
}),
|
content?.emit("error", e) ?? reject(e);
|
||||||
);
|
rejectChecksum(e);
|
||||||
bb.on(
|
});
|
||||||
"file",
|
});
|
||||||
handler(async (fieldname, file) => {
|
|
||||||
if (fieldname !== "content") error(400, "Invalid request body");
|
|
||||||
if (!metadata || content) error(400, "Invalid request body");
|
|
||||||
content = file;
|
|
||||||
|
|
||||||
await uploadFile(metadata, content);
|
|
||||||
resolve(text("File uploaded", { headers: { "Content-Type": "text/plain" } }));
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
bb.on("error", (e) => content?.emit("error", e) ?? reject(e));
|
|
||||||
|
|
||||||
request.body!.pipeTo(Writable.toWeb(bb)).catch(() => {}); // busboy will handle the error
|
request.body!.pipeTo(Writable.toWeb(bb)).catch(() => {}); // busboy will handle the error
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user