/api/hsk/list, /api/hsk/register/initial Endpoint 구현

This commit is contained in:
static
2025-01-12 20:26:48 +09:00
parent 004e41b0cf
commit 805d7df182
13 changed files with 223 additions and 4 deletions

View File

@@ -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"

View File

@@ -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,

45
src/lib/server/db/hsk.ts Normal file
View File

@@ -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")));
};

View File

@@ -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],
}),
}),
);

View File

@@ -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],
}),
}),
);

View File

@@ -1,5 +1,6 @@
export * from "./client";
export * from "./file";
export * from "./hsk";
export * from "./mek";
export * from "./session";
export * from "./user";

View File

@@ -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()

View File

@@ -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<typeof hmacSecretListResponse>;
export const initialHmacSecretRegisterRequest = z.object({
mekVersion: z.number().int().positive(),
hsk: z.string().base64().nonempty(),
});
export type InitialHmacSecretRegisterRequest = z.infer<typeof initialHmacSecretRegisterRequest>;

View File

@@ -2,4 +2,5 @@ export * from "./auth";
export * from "./client";
export * from "./directory";
export * from "./file";
export * from "./hsk";
export * from "./mek";

View File

@@ -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;
}
};

View File

@@ -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,

View File

@@ -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),
);
};

View File

@@ -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" } });
};