diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index ebd1815..6dcf925 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -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() diff --git a/src/lib/server/db/mek.ts b/src/lib/server/db/mek.ts new file mode 100644 index 0000000..54ca67f --- /dev/null +++ b/src/lib/server/db/mek.ts @@ -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(); +}; diff --git a/src/lib/server/db/schema/mek.ts b/src/lib/server/db/schema/mek.ts index e7314bd..f3f8b6b 100644 --- a/src/lib/server/db/schema/mek.ts +++ b/src/lib/server/db/schema/mek.ts @@ -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], - }), - }), -); diff --git a/src/lib/server/services/mek.ts b/src/lib/server/services/mek.ts new file mode 100644 index 0000000..d67f52d --- /dev/null +++ b/src/lib/server/services/mek.ts @@ -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); +}; diff --git a/src/routes/api/mek/list/+server.ts b/src/routes/api/mek/list/+server.ts new file mode 100644 index 0000000..35184e9 --- /dev/null +++ b/src/routes/api/mek/list/+server.ts @@ -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 }); +}; diff --git a/src/routes/api/mek/register/+server.ts b/src/routes/api/mek/register/+server.ts new file mode 100644 index 0000000..21ed7b2 --- /dev/null +++ b/src/routes/api/mek/register/+server.ts @@ -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" } }); +};