From 805d7df182e8acde734e71b596d8b6d1cf447d04 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 12 Jan 2025 20:26:48 +0900 Subject: [PATCH] =?UTF-8?q?/api/hsk/list,=20/api/hsk/register/initial=20En?= =?UTF-8?q?dpoint=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/error.ts | 3 ++ src/lib/server/db/file.ts | 21 ++++++++- src/lib/server/db/hsk.ts | 45 +++++++++++++++++++ src/lib/server/db/schema/file.ts | 9 +++- src/lib/server/db/schema/hsk.ts | 43 ++++++++++++++++++ src/lib/server/db/schema/index.ts | 1 + src/lib/server/schemas/file.ts | 2 + src/lib/server/schemas/hsk.ts | 19 ++++++++ src/lib/server/schemas/index.ts | 1 + src/lib/server/services/hsk.ts | 31 +++++++++++++ src/routes/api/file/upload/+server.ts | 16 ++++++- src/routes/api/hsk/list/+server.ts | 20 +++++++++ .../api/hsk/register/initial/+server.ts | 16 +++++++ 13 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 src/lib/server/db/hsk.ts create mode 100644 src/lib/server/db/schema/hsk.ts create mode 100644 src/lib/server/schemas/hsk.ts create mode 100644 src/lib/server/services/hsk.ts create mode 100644 src/routes/api/hsk/list/+server.ts create mode 100644 src/routes/api/hsk/register/initial/+server.ts diff --git a/src/lib/server/db/error.ts b/src/lib/server/db/error.ts index beadb6f..547cc6c 100644 --- a/src/lib/server/db/error.ts +++ b/src/lib/server/db/error.ts @@ -8,6 +8,9 @@ type IntegrityErrorMessages = | "Directory not found" | "File not found" | "Invalid DEK version" + // HSK + | "HSK already registered" + | "Inactive HSK version" // MEK | "MEK already registered" | "Inactive MEK version" diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index edf0249..1ce230c 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -1,7 +1,7 @@ import { and, eq, isNull } from "drizzle-orm"; import db from "./drizzle"; import { IntegrityError } from "./error"; -import { directory, directoryLog, file, fileLog, mek } from "./schema"; +import { directory, directoryLog, file, fileLog, hsk, mek } from "./schema"; type DirectoryId = "root" | number; @@ -22,6 +22,8 @@ export interface NewFileParams { mekVersion: number; encDek: string; dekVersion: Date; + hskVersion: number | null; + contentHmac: string | null; contentType: string; encContentIv: string; encName: string; @@ -152,6 +154,10 @@ export const unregisterDirectory = async (userId: number, directoryId: number) = }; export const registerFile = async (params: NewFileParams) => { + if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) { + throw new Error("Invalid arguments"); + } + await db.transaction( async (tx) => { const meks = await tx @@ -163,6 +169,17 @@ export const registerFile = async (params: NewFileParams) => { throw new IntegrityError("Inactive MEK version"); } + if (params.hskVersion) { + const hsks = await tx + .select({ version: hsk.version }) + .from(hsk) + .where(and(eq(hsk.userId, params.userId), eq(hsk.state, "active"))) + .limit(1); + if (hsks[0]?.version !== params.hskVersion) { + throw new IntegrityError("Inactive HSK version"); + } + } + const newFiles = await tx .insert(file) .values({ @@ -170,6 +187,8 @@ export const registerFile = async (params: NewFileParams) => { parentId: params.parentId === "root" ? null : params.parentId, userId: params.userId, mekVersion: params.mekVersion, + hskVersion: params.hskVersion, + contentHmac: params.contentHmac, contentType: params.contentType, encDek: params.encDek, dekVersion: params.dekVersion, diff --git a/src/lib/server/db/hsk.ts b/src/lib/server/db/hsk.ts new file mode 100644 index 0000000..809ed7b --- /dev/null +++ b/src/lib/server/db/hsk.ts @@ -0,0 +1,45 @@ +import { SqliteError } from "better-sqlite3"; +import { and, eq } from "drizzle-orm"; +import db from "./drizzle"; +import { IntegrityError } from "./error"; +import { hsk, hskLog } from "./schema"; + +export const registerInitialHsk = async ( + userId: number, + createdBy: number, + mekVersion: number, + encHsk: string, +) => { + await db.transaction( + async (tx) => { + try { + await tx.insert(hsk).values({ + userId, + version: 1, + state: "active", + mekVersion, + encHsk, + }); + await tx.insert(hskLog).values({ + userId, + hskVersion: 1, + timestamp: new Date(), + action: "create", + actionBy: createdBy, + }); + } catch (e) { + if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") { + throw new IntegrityError("HSK already registered"); + } + } + }, + { behavior: "exclusive" }, + ); +}; + +export const getAllValidHsks = async (userId: number) => { + return await db + .select() + .from(hsk) + .where(and(eq(hsk.userId, userId), eq(hsk.state, "active"))); +}; diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts index 0bae91a..7ac0b77 100644 --- a/src/lib/server/db/schema/file.ts +++ b/src/lib/server/db/schema/file.ts @@ -1,4 +1,5 @@ import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core"; +import { hsk } from "./hsk"; import { mek } from "./mek"; import { user } from "./user"; @@ -55,15 +56,21 @@ export const file = sqliteTable( mekVersion: integer("master_encryption_key_version").notNull(), encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(), + hskVersion: integer("hmac_secret_key_version"), + contentHmac: text("content_hmac"), // Base64 contentType: text("content_type").notNull(), encContentIv: text("encrypted_content_iv").notNull(), // Base64 encName: ciphertext("encrypted_name").notNull(), }, (t) => ({ - ref: foreignKey({ + ref1: foreignKey({ columns: [t.userId, t.mekVersion], foreignColumns: [mek.userId, mek.version], }), + ref2: foreignKey({ + columns: [t.userId, t.hskVersion], + foreignColumns: [hsk.userId, hsk.version], + }), }), ); diff --git a/src/lib/server/db/schema/hsk.ts b/src/lib/server/db/schema/hsk.ts new file mode 100644 index 0000000..b78c512 --- /dev/null +++ b/src/lib/server/db/schema/hsk.ts @@ -0,0 +1,43 @@ +import { sqliteTable, text, integer, primaryKey, foreignKey } from "drizzle-orm/sqlite-core"; +import { mek } from "./mek"; +import { user } from "./user"; + +export const hsk = sqliteTable( + "hmac_secret_key", + { + userId: integer("user_id") + .notNull() + .references(() => user.id), + version: integer("version").notNull(), + state: text("state", { enum: ["active"] }).notNull(), + mekVersion: integer("master_encryption_key_version").notNull(), + encHsk: text("encrypted_key").notNull().unique(), // Base64 + }, + (t) => ({ + pk: primaryKey({ columns: [t.userId, t.version] }), + ref: foreignKey({ + columns: [t.userId, t.mekVersion], + foreignColumns: [mek.userId, mek.version], + }), + }), +); + +export const hskLog = sqliteTable( + "hmac_secret_key_log", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: integer("user_id") + .notNull() + .references(() => user.id), + hskVersion: integer("hmac_secret_key_version").notNull(), + timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(), + action: text("action", { enum: ["create"] }).notNull(), + actionBy: integer("action_by").references(() => user.id), + }, + (t) => ({ + ref: foreignKey({ + columns: [t.userId, t.hskVersion], + foreignColumns: [hsk.userId, hsk.version], + }), + }), +); diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 41fd4fe..40cb9be 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -1,5 +1,6 @@ export * from "./client"; export * from "./file"; +export * from "./hsk"; export * from "./mek"; export * from "./session"; export * from "./user"; diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 0592835..958c4d7 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -27,6 +27,8 @@ export const fileUploadRequest = z.object({ mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), + hskVersion: z.number().int().positive(), + contentHmac: z.string().base64().nonempty(), contentType: z .string() .nonempty() diff --git a/src/lib/server/schemas/hsk.ts b/src/lib/server/schemas/hsk.ts new file mode 100644 index 0000000..bcea3cd --- /dev/null +++ b/src/lib/server/schemas/hsk.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const hmacSecretListResponse = z.object({ + hsks: z.array( + z.object({ + version: z.number().int().positive(), + state: z.enum(["active"]), + mekVersion: z.number().int().positive(), + hsk: z.string().base64().nonempty(), + }), + ), +}); +export type HmacSecretListResponse = z.infer; + +export const initialHmacSecretRegisterRequest = z.object({ + mekVersion: z.number().int().positive(), + hsk: z.string().base64().nonempty(), +}); +export type InitialHmacSecretRegisterRequest = z.infer; diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index cd2a366..615e2bc 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -2,4 +2,5 @@ export * from "./auth"; export * from "./client"; export * from "./directory"; export * from "./file"; +export * from "./hsk"; export * from "./mek"; diff --git a/src/lib/server/services/hsk.ts b/src/lib/server/services/hsk.ts new file mode 100644 index 0000000..c381c51 --- /dev/null +++ b/src/lib/server/services/hsk.ts @@ -0,0 +1,31 @@ +import { error } from "@sveltejs/kit"; +import { IntegrityError } from "$lib/server/db/error"; +import { registerInitialHsk, getAllValidHsks } from "$lib/server/db/hsk"; + +export const getHskList = async (userId: number) => { + const hsks = await getAllValidHsks(userId); + return { + encHsks: hsks.map(({ version, state, mekVersion, encHsk }) => ({ + version, + state, + mekVersion, + encHsk, + })), + }; +}; + +export const registerInitialActiveHsk = async ( + userId: number, + createdBy: number, + mekVersion: number, + encHsk: string, +) => { + try { + await registerInitialHsk(userId, createdBy, mekVersion, encHsk); + } catch (e) { + if (e instanceof IntegrityError && e.message === "HSK already registered") { + error(409, "Initial HSK already registered"); + } + throw e; + } +}; diff --git a/src/routes/api/file/upload/+server.ts b/src/routes/api/file/upload/+server.ts index ac1ac51..1bc9ce2 100644 --- a/src/routes/api/file/upload/+server.ts +++ b/src/routes/api/file/upload/+server.ts @@ -16,8 +16,18 @@ export const POST: RequestHandler = async ({ locals, request }) => { const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata)); if (!zodRes.success) error(400, "Invalid request body"); - const { parentId, mekVersion, dek, dekVersion, contentType, contentIv, name, nameIv } = - zodRes.data; + const { + parentId, + mekVersion, + dek, + dekVersion, + hskVersion, + contentHmac, + contentType, + contentIv, + name, + nameIv, + } = zodRes.data; await uploadFile( { @@ -26,6 +36,8 @@ export const POST: RequestHandler = async ({ locals, request }) => { mekVersion, encDek: dek, dekVersion: new Date(dekVersion), + hskVersion, + contentHmac, contentType, encContentIv: contentIv, encName: name, diff --git a/src/routes/api/hsk/list/+server.ts b/src/routes/api/hsk/list/+server.ts new file mode 100644 index 0000000..50957a3 --- /dev/null +++ b/src/routes/api/hsk/list/+server.ts @@ -0,0 +1,20 @@ +import { json } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { hmacSecretListResponse, type HmacSecretListResponse } from "$lib/server/schemas"; +import { getHskList } from "$lib/server/services/hsk"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ locals }) => { + const { userId } = await authorize(locals, "activeClient"); + const { encHsks } = await getHskList(userId); + return json( + hmacSecretListResponse.parse({ + hsks: encHsks.map(({ version, state, mekVersion, encHsk }) => ({ + version, + state, + mekVersion, + hsk: encHsk, + })), + } satisfies HmacSecretListResponse), + ); +}; diff --git a/src/routes/api/hsk/register/initial/+server.ts b/src/routes/api/hsk/register/initial/+server.ts new file mode 100644 index 0000000..8b32952 --- /dev/null +++ b/src/routes/api/hsk/register/initial/+server.ts @@ -0,0 +1,16 @@ +import { error, text } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { initialHmacSecretRegisterRequest } from "$lib/server/schemas"; +import { registerInitialActiveHsk } from "$lib/server/services/hsk"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId, clientId } = await authorize(locals, "activeClient"); + + const zodRes = initialHmacSecretRegisterRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { mekVersion, hsk } = zodRes.data; + + await registerInitialActiveHsk(userId, clientId, mekVersion, hsk); + return text("HSK registered", { headers: { "Content-Type": "text/plain" } }); +};