mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-15 22:38:47 +00:00
/api/hsk/list, /api/hsk/register/initial Endpoint 구현
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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
45
src/lib/server/db/hsk.ts
Normal 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")));
|
||||
};
|
||||
@@ -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],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
43
src/lib/server/db/schema/hsk.ts
Normal file
43
src/lib/server/db/schema/hsk.ts
Normal 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],
|
||||
}),
|
||||
}),
|
||||
);
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from "./client";
|
||||
export * from "./file";
|
||||
export * from "./hsk";
|
||||
export * from "./mek";
|
||||
export * from "./session";
|
||||
export * from "./user";
|
||||
|
||||
@@ -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()
|
||||
|
||||
19
src/lib/server/schemas/hsk.ts
Normal file
19
src/lib/server/schemas/hsk.ts
Normal 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>;
|
||||
@@ -2,4 +2,5 @@ export * from "./auth";
|
||||
export * from "./client";
|
||||
export * from "./directory";
|
||||
export * from "./file";
|
||||
export * from "./hsk";
|
||||
export * from "./mek";
|
||||
|
||||
31
src/lib/server/services/hsk.ts
Normal file
31
src/lib/server/services/hsk.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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,
|
||||
|
||||
20
src/routes/api/hsk/list/+server.ts
Normal file
20
src/routes/api/hsk/list/+server.ts
Normal 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),
|
||||
);
|
||||
};
|
||||
16
src/routes/api/hsk/register/initial/+server.ts
Normal file
16
src/routes/api/hsk/register/initial/+server.ts
Normal 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" } });
|
||||
};
|
||||
Reference in New Issue
Block a user