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"
|
| "Directory not found"
|
||||||
| "File not found"
|
| "File not found"
|
||||||
| "Invalid DEK version"
|
| "Invalid DEK version"
|
||||||
|
// HSK
|
||||||
|
| "HSK already registered"
|
||||||
|
| "Inactive HSK version"
|
||||||
// MEK
|
// MEK
|
||||||
| "MEK already registered"
|
| "MEK already registered"
|
||||||
| "Inactive MEK version"
|
| "Inactive MEK version"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { and, eq, isNull } from "drizzle-orm";
|
import { and, eq, isNull } from "drizzle-orm";
|
||||||
import db from "./drizzle";
|
import db from "./drizzle";
|
||||||
import { IntegrityError } from "./error";
|
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;
|
type DirectoryId = "root" | number;
|
||||||
|
|
||||||
@@ -22,6 +22,8 @@ export interface NewFileParams {
|
|||||||
mekVersion: number;
|
mekVersion: number;
|
||||||
encDek: string;
|
encDek: string;
|
||||||
dekVersion: Date;
|
dekVersion: Date;
|
||||||
|
hskVersion: number | null;
|
||||||
|
contentHmac: string | null;
|
||||||
contentType: string;
|
contentType: string;
|
||||||
encContentIv: string;
|
encContentIv: string;
|
||||||
encName: string;
|
encName: string;
|
||||||
@@ -152,6 +154,10 @@ export const unregisterDirectory = async (userId: number, directoryId: number) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const registerFile = async (params: NewFileParams) => {
|
export const registerFile = async (params: NewFileParams) => {
|
||||||
|
if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) {
|
||||||
|
throw new Error("Invalid arguments");
|
||||||
|
}
|
||||||
|
|
||||||
await db.transaction(
|
await db.transaction(
|
||||||
async (tx) => {
|
async (tx) => {
|
||||||
const meks = await tx
|
const meks = await tx
|
||||||
@@ -163,6 +169,17 @@ export const registerFile = async (params: NewFileParams) => {
|
|||||||
throw new IntegrityError("Inactive MEK version");
|
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
|
const newFiles = await tx
|
||||||
.insert(file)
|
.insert(file)
|
||||||
.values({
|
.values({
|
||||||
@@ -170,6 +187,8 @@ export const registerFile = async (params: NewFileParams) => {
|
|||||||
parentId: params.parentId === "root" ? null : params.parentId,
|
parentId: params.parentId === "root" ? null : params.parentId,
|
||||||
userId: params.userId,
|
userId: params.userId,
|
||||||
mekVersion: params.mekVersion,
|
mekVersion: params.mekVersion,
|
||||||
|
hskVersion: params.hskVersion,
|
||||||
|
contentHmac: params.contentHmac,
|
||||||
contentType: params.contentType,
|
contentType: params.contentType,
|
||||||
encDek: params.encDek,
|
encDek: params.encDek,
|
||||||
dekVersion: params.dekVersion,
|
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 { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core";
|
||||||
|
import { hsk } from "./hsk";
|
||||||
import { mek } from "./mek";
|
import { mek } from "./mek";
|
||||||
import { user } from "./user";
|
import { user } from "./user";
|
||||||
|
|
||||||
@@ -55,15 +56,21 @@ export const file = sqliteTable(
|
|||||||
mekVersion: integer("master_encryption_key_version").notNull(),
|
mekVersion: integer("master_encryption_key_version").notNull(),
|
||||||
encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64
|
encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64
|
||||||
dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(),
|
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(),
|
contentType: text("content_type").notNull(),
|
||||||
encContentIv: text("encrypted_content_iv").notNull(), // Base64
|
encContentIv: text("encrypted_content_iv").notNull(), // Base64
|
||||||
encName: ciphertext("encrypted_name").notNull(),
|
encName: ciphertext("encrypted_name").notNull(),
|
||||||
},
|
},
|
||||||
(t) => ({
|
(t) => ({
|
||||||
ref: foreignKey({
|
ref1: foreignKey({
|
||||||
columns: [t.userId, t.mekVersion],
|
columns: [t.userId, t.mekVersion],
|
||||||
foreignColumns: [mek.userId, mek.version],
|
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 "./client";
|
||||||
export * from "./file";
|
export * from "./file";
|
||||||
|
export * from "./hsk";
|
||||||
export * from "./mek";
|
export * from "./mek";
|
||||||
export * from "./session";
|
export * from "./session";
|
||||||
export * from "./user";
|
export * from "./user";
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export const fileUploadRequest = z.object({
|
|||||||
mekVersion: z.number().int().positive(),
|
mekVersion: z.number().int().positive(),
|
||||||
dek: z.string().base64().nonempty(),
|
dek: z.string().base64().nonempty(),
|
||||||
dekVersion: z.string().datetime(),
|
dekVersion: z.string().datetime(),
|
||||||
|
hskVersion: z.number().int().positive(),
|
||||||
|
contentHmac: z.string().base64().nonempty(),
|
||||||
contentType: z
|
contentType: z
|
||||||
.string()
|
.string()
|
||||||
.nonempty()
|
.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 "./client";
|
||||||
export * from "./directory";
|
export * from "./directory";
|
||||||
export * from "./file";
|
export * from "./file";
|
||||||
|
export * from "./hsk";
|
||||||
export * from "./mek";
|
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));
|
const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata));
|
||||||
if (!zodRes.success) error(400, "Invalid request body");
|
if (!zodRes.success) error(400, "Invalid request body");
|
||||||
const { parentId, mekVersion, dek, dekVersion, contentType, contentIv, name, nameIv } =
|
const {
|
||||||
zodRes.data;
|
parentId,
|
||||||
|
mekVersion,
|
||||||
|
dek,
|
||||||
|
dekVersion,
|
||||||
|
hskVersion,
|
||||||
|
contentHmac,
|
||||||
|
contentType,
|
||||||
|
contentIv,
|
||||||
|
name,
|
||||||
|
nameIv,
|
||||||
|
} = zodRes.data;
|
||||||
|
|
||||||
await uploadFile(
|
await uploadFile(
|
||||||
{
|
{
|
||||||
@@ -26,6 +36,8 @@ export const POST: RequestHandler = async ({ locals, request }) => {
|
|||||||
mekVersion,
|
mekVersion,
|
||||||
encDek: dek,
|
encDek: dek,
|
||||||
dekVersion: new Date(dekVersion),
|
dekVersion: new Date(dekVersion),
|
||||||
|
hskVersion,
|
||||||
|
contentHmac,
|
||||||
contentType,
|
contentType,
|
||||||
encContentIv: contentIv,
|
encContentIv: contentIv,
|
||||||
encName: name,
|
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