/api/mek/list, /api/mek/register Endpoint 구현

This commit is contained in:
static
2024-12-29 21:52:33 +09:00
parent 3664ad66ac
commit 97f6e1e32f
6 changed files with 186 additions and 24 deletions

View File

@@ -21,6 +21,14 @@ export const createUserClient = async (userId: number, clientId: number) => {
await db.insert(userClient).values({ userId, clientId }).execute();
};
export const getAllValidUserClients = async (userId: number) => {
return await db
.select()
.from(userClient)
.where(and(eq(userClient.userId, userId), eq(userClient.state, "active")))
.execute();
};
export const getUserClient = async (userId: number, clientId: number) => {
const userClients = await db
.select()

88
src/lib/server/db/mek.ts Normal file
View File

@@ -0,0 +1,88 @@
import { and, or, eq, lt } from "drizzle-orm";
import db from "./drizzle";
import { mek, clientMek, userClient } from "./schema";
export interface ClientMek {
clientId: number;
encMek: string;
}
export const registerActiveMek = async (
userId: number,
version: number,
createdBy: number,
clientMeks: ClientMek[],
) => {
await db.transaction(async (tx) => {
// 1. Check if the clientMeks are valid
const userClients = await tx
.select()
.from(userClient)
.where(and(eq(userClient.userId, userId), eq(userClient.state, "active")));
if (
clientMeks.length !== userClients.length ||
!clientMeks.every((clientMek) =>
userClients.some((userClient) => userClient.clientId === clientMek.clientId),
)
) {
throw new Error("Invalid key list");
}
// 2. Retire the old active MEK and insert the new one
await tx
.update(mek)
.set({
state: "retired",
retiredAt: new Date(),
})
.where(and(eq(mek.userId, userId), lt(mek.version, version), eq(mek.state, "active")))
.execute();
await tx
.insert(mek)
.values({
userId,
version,
createdBy,
createdAt: new Date(),
state: "active",
})
.execute();
// 3. Insert the new client MEKs
await tx
.insert(clientMek)
.values(
clientMeks.map(({ clientId, encMek }) => ({
userId,
clientId,
mekVersion: version,
encMek,
})),
)
.execute();
});
};
export const getActiveMek = async (userId: number) => {
const meks = await db
.select()
.from(mek)
.where(and(eq(mek.userId, userId), eq(mek.state, "active")))
.execute();
return meks[0] ?? null;
};
export const getAllValidClientMeks = async (userId: number, clientId: number) => {
return await db
.select()
.from(clientMek)
.innerJoin(mek, and(eq(clientMek.userId, mek.userId), eq(clientMek.mekVersion, mek.version)))
.where(
and(
eq(clientMek.userId, userId),
eq(clientMek.clientId, clientId),
or(eq(mek.state, "active"), eq(mek.state, "retired")),
),
)
.execute();
};

View File

@@ -13,9 +13,8 @@ export const mek = sqliteTable(
.notNull()
.references(() => client.id),
createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(),
state: text("state", { enum: ["pending", "active", "retired", "dead"] })
.notNull()
.default("pending"),
state: text("state", { enum: ["active", "retired", "dead"] }).notNull(),
retiredAt: integer("retired_at", { mode: "timestamp_ms" }),
},
(t) => ({
pk: primaryKey({ columns: [t.userId, t.version] }),
@@ -42,24 +41,3 @@ export const clientMek = sqliteTable(
}),
}),
);
export const mekChallenge = sqliteTable(
"master_encryption_key_challenge",
{
userId: integer("user_id")
.notNull()
.references(() => user.id),
mekVersion: integer("master_encryption_key_version").notNull(),
answer: text("answer").notNull().unique(), // Base64
challenge: text("challenge").unique(), // Base64
allowedIp: text("allowed_ip").notNull(),
expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(),
},
(t) => ({
pk: primaryKey({ columns: [t.userId, t.mekVersion] }),
ref: foreignKey({
columns: [t.userId, t.mekVersion],
foreignColumns: [mek.userId, mek.version],
}),
}),
);

View File

@@ -0,0 +1,39 @@
import { error } from "@sveltejs/kit";
import { getAllValidUserClients } from "$lib/server/db/client";
import {
getAllValidClientMeks,
getActiveMek,
registerActiveMek,
type ClientMek,
} from "$lib/server/db/mek";
export const getClientMekList = async (userId: number, clientId: number) => {
const clientMeks = await getAllValidClientMeks(userId, clientId);
return {
meks: clientMeks.map((clientMek) => ({
version: clientMek.master_encryption_key.version,
state: clientMek.master_encryption_key.state,
mek: clientMek.client_master_encryption_key.encMek,
})),
};
};
export const registerNewActiveMek = async (
userId: number,
createdBy: number,
clientMeks: ClientMek[],
) => {
const userClients = await getAllValidUserClients(userId);
if (
clientMeks.length !== userClients.length ||
!clientMeks.every((clientMek) =>
userClients.some((userClient) => userClient.clientId === clientMek.clientId),
)
) {
error(400, "Invalid key list");
}
const oldActiveMek = await getActiveMek(userId);
const newMekVersion = (oldActiveMek?.version ?? 0) + 1;
await registerActiveMek(userId, newMekVersion, createdBy, clientMeks);
};

View File

@@ -0,0 +1,14 @@
import { error, json } from "@sveltejs/kit";
import { authenticate } from "$lib/server/modules/auth";
import { getClientMekList } from "$lib/server/services/mek";
import type { RequestHandler } from "@sveltejs/kit";
export const GET: RequestHandler = async ({ cookies }) => {
const { userId, clientId } = authenticate(cookies);
if (!clientId) {
error(403, "Forbidden");
}
const { meks } = await getClientMekList(userId, clientId);
return json({ meks });
};

View File

@@ -0,0 +1,35 @@
import { error, text } from "@sveltejs/kit";
import { z } from "zod";
import { authenticate } from "$lib/server/modules/auth";
import { registerNewActiveMek } from "$lib/server/services/mek";
import type { RequestHandler } from "@sveltejs/kit";
export const POST: RequestHandler = async ({ request, cookies }) => {
const zodRes = z
.object({
meks: z.array(
z.object({
clientId: z.number(),
mek: z.string().base64().nonempty(),
}),
),
})
.safeParse(await request.json());
if (!zodRes.success) error(400, "Invalid request body");
const { userId, clientId } = authenticate(cookies);
if (!clientId) {
error(403, "Forbidden");
}
const { meks } = zodRes.data;
await registerNewActiveMek(
userId,
clientId,
meks.map(({ clientId, mek }) => ({
clientId,
encMek: mek.trim(),
})),
);
return text("MEK registered", { headers: { "Content-Type": "text/plain" } });
};