From ccad4fbd8b198372f1dcc7b1eafe2bd8c7de0173 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 17:41:54 +0900 Subject: [PATCH] =?UTF-8?q?/api/client/[id]/key,=20/api/mek/share=20Endpoi?= =?UTF-8?q?nt=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/client.ts | 10 ++ src/lib/server/db/mek.ts | 137 ++++++++++++++-------- src/lib/server/db/schema/client.ts | 2 +- src/lib/server/db/schema/user.ts | 2 +- src/lib/server/services/client.ts | 10 ++ src/lib/server/services/mek.ts | 33 ++++-- src/routes/api/client/[id]/key/+server.ts | 20 ++++ src/routes/api/mek/share/+server.ts | 30 +++++ 8 files changed, 187 insertions(+), 57 deletions(-) create mode 100644 src/routes/api/client/[id]/key/+server.ts create mode 100644 src/routes/api/mek/share/+server.ts diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index 29f4806..bdf2404 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -62,6 +62,16 @@ export const getUserClient = async (userId: number, clientId: number) => { return userClients[0] ?? null; }; +export const getUserClientWithDetails = async (userId: number, clientId: number) => { + const userClients = await db + .select() + .from(userClient) + .innerJoin(client, eq(userClient.clientId, client.id)) + .where(and(eq(userClient.userId, userId), eq(userClient.clientId, clientId))) + .execute(); + return userClients[0] ?? null; +}; + export const setUserClientStateToPending = async (userId: number, clientId: number) => { await db .update(userClient) diff --git a/src/lib/server/db/mek.ts b/src/lib/server/db/mek.ts index 36b0042..3c0bfff 100644 --- a/src/lib/server/db/mek.ts +++ b/src/lib/server/db/mek.ts @@ -2,32 +2,21 @@ import { and, or, eq, lt, desc } from "drizzle-orm"; import db from "./drizzle"; import { mek, clientMek, userClient } from "./schema"; -interface ClientMek { - clientId: number; - encMek: string; -} - export const registerInitialMek = async (userId: number, createdBy: number, encMek: string) => { await db.transaction(async (tx) => { - await tx - .insert(mek) - .values({ - userId, - version: 1, - createdBy, - createdAt: new Date(), - state: "active", - }) - .execute(); - await tx - .insert(clientMek) - .values({ - userId, - clientId: createdBy, - mekVersion: 1, - encMek, - }) - .execute(); + await tx.insert(mek).values({ + userId, + version: 1, + createdBy, + createdAt: new Date(), + state: "active", + }); + await tx.insert(clientMek).values({ + userId, + clientId: createdBy, + mekVersion: 1, + encMek, + }); }); }; @@ -35,7 +24,10 @@ export const registerActiveMek = async ( userId: number, version: number, createdBy: number, - clientMeks: ClientMek[], + clientMeks: { + clientId: number; + encMek: string; + }[], ) => { await db.transaction(async (tx) => { // 1. Check if the clientMeks are valid @@ -59,34 +51,35 @@ export const registerActiveMek = async ( 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(); + .where(and(eq(mek.userId, userId), lt(mek.version, version), eq(mek.state, "active"))); + await tx.insert(mek).values({ + userId, + version, + createdBy, + createdAt: new Date(), + state: "active", + }); // 3. Insert the new client MEKs - await tx - .insert(clientMek) - .values( - clientMeks.map(({ clientId, encMek }) => ({ - userId, - clientId, - mekVersion: version, - encMek, - })), - ) - .execute(); + await tx.insert(clientMek).values( + clientMeks.map(({ clientId, encMek }) => ({ + userId, + clientId, + mekVersion: version, + encMek, + })), + ); }); }; +export const getAllValidMeks = async (userId: number) => { + return await db + .select() + .from(mek) + .where(and(eq(mek.userId, userId), or(eq(mek.state, "active"), eq(mek.state, "retired")))) + .execute(); +}; + export const getInitialMek = async (userId: number) => { const meks = await db .select() @@ -110,6 +103,54 @@ export const getNextActiveMekVersion = async (userId: number) => { return meks[0].version + 1; }; +export const registerClientMeks = async ( + userId: number, + clientId: number, + clientMeks: { + version: number; + encMek: string; + }[], +) => { + await db.transaction(async (tx) => { + // 1. Check if the client is valid + const userClients = await tx + .select() + .from(userClient) + .where( + and( + eq(userClient.userId, userId), + eq(userClient.clientId, clientId), + eq(userClient.state, "active"), + ), + ); + if (userClients.length === 0) { + throw new Error("Invalid client"); + } + + // 2. Check if the clientMeks are valid + const meks = await tx + .select() + .from(mek) + .where(and(eq(mek.userId, userId), or(eq(mek.state, "active"), eq(mek.state, "retired")))); + if ( + clientMeks.length !== meks.length || + !clientMeks.every((clientMek) => meks.some((mek) => mek.version === clientMek.version)) + ) { + throw new Error("Invalid key list"); + } + + // 3. Insert the client MEKs + await tx.insert(clientMek).values( + clientMeks.map(({ version, encMek }) => ({ + userId, + clientId, + mekVersion: version, + encMek, + })), + ); + }); +}; + export const getAllValidClientMeks = async (userId: number, clientId: number) => { return await db .select() diff --git a/src/lib/server/db/schema/client.ts b/src/lib/server/db/schema/client.ts index 7d83435..437695d 100644 --- a/src/lib/server/db/schema/client.ts +++ b/src/lib/server/db/schema/client.ts @@ -4,7 +4,7 @@ import { user } from "./user"; export const client = sqliteTable( "client", { - id: integer("id").primaryKey(), + id: integer("id").primaryKey({ autoIncrement: true }), encPubKey: text("encryption_public_key").notNull().unique(), // Base64 sigPubKey: text("signature_public_key").notNull().unique(), // Base64 }, diff --git a/src/lib/server/db/schema/user.ts b/src/lib/server/db/schema/user.ts index 2ad0e3c..5d70e00 100644 --- a/src/lib/server/db/schema/user.ts +++ b/src/lib/server/db/schema/user.ts @@ -1,7 +1,7 @@ import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; export const user = sqliteTable("user", { - id: integer("id").primaryKey(), + id: integer("id").primaryKey({ autoIncrement: true }), email: text("email").notNull().unique(), password: text("password").notNull(), }); diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts index 004ef1a..8893973 100644 --- a/src/lib/server/services/client.ts +++ b/src/lib/server/services/client.ts @@ -8,6 +8,7 @@ import { createUserClient, getAllUserClients, getUserClient, + getUserClientWithDetails, setUserClientStateToPending, registerUserClientChallenge, getUserClientChallenge, @@ -17,6 +18,15 @@ import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/mo import { isInitialMekNeeded } from "$lib/server/modules/mek"; import env from "$lib/server/loadenv"; +export const getUserClientEncPubKey = async (userId: number, clientId: number) => { + const userClient = await getUserClientWithDetails(userId, clientId); + if (!userClient || userClient.user_client.state === "challenging") { + error(400, "Invalid client ID"); + } + + return { encPubKey: userClient.client.encPubKey }; +}; + export const getUserClientList = async (userId: number) => { const userClients = await getAllUserClients(userId); return { diff --git a/src/lib/server/services/mek.ts b/src/lib/server/services/mek.ts index f2de646..a3c8449 100644 --- a/src/lib/server/services/mek.ts +++ b/src/lib/server/services/mek.ts @@ -1,18 +1,15 @@ import { error } from "@sveltejs/kit"; import { getAllUserClients, setUserClientStateToActive } from "$lib/server/db/client"; import { - getAllValidClientMeks, registerInitialMek, registerActiveMek, + getAllValidMeks, getNextActiveMekVersion, + registerClientMeks, + getAllValidClientMeks, } from "$lib/server/db/mek"; import { isInitialMekNeeded } from "$lib/server/modules/mek"; -interface NewClientMek { - clientId: number; - encMek: string; -} - export const getClientMekList = async (userId: number, clientId: number) => { const clientMeks = await getAllValidClientMeks(userId, clientId); return { @@ -40,7 +37,10 @@ export const registerInitialActiveMek = async ( export const registerNewActiveMek = async ( userId: number, createdBy: number, - clientMeks: NewClientMek[], + clientMeks: { + clientId: number; + encMek: string; + }[], ) => { const userClients = await getAllUserClients(userId); const activeUserClients = userClients.filter(({ state }) => state === "active"); @@ -56,3 +56,22 @@ export const registerNewActiveMek = async ( const newMekVersion = await getNextActiveMekVersion(userId); await registerActiveMek(userId, newMekVersion, createdBy, clientMeks); }; + +export const shareMeksForNewClient = async ( + userId: number, + targetClientId: number, + clientMeks: { + version: number; + encMek: string; + }[], +) => { + const meks = await getAllValidMeks(userId); + if ( + clientMeks.length !== meks.length || + !clientMeks.every((clientMek) => meks.some((mek) => mek.version === clientMek.version)) + ) { + error(400, "Invalid key list"); + } + + await registerClientMeks(userId, targetClientId, clientMeks); +}; diff --git a/src/routes/api/client/[id]/key/+server.ts b/src/routes/api/client/[id]/key/+server.ts new file mode 100644 index 0000000..8e031f9 --- /dev/null +++ b/src/routes/api/client/[id]/key/+server.ts @@ -0,0 +1,20 @@ +import { error, json } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { getUserClientEncPubKey } from "$lib/server/services/client"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ cookies, params }) => { + const { userId } = await authorize(cookies, "activeClient"); + + const zodRes = z + .object({ + id: z.coerce.number().int().positive(), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + + const { encPubKey } = await getUserClientEncPubKey(userId, id); + return json({ encPubKey }); +}; diff --git a/src/routes/api/mek/share/+server.ts b/src/routes/api/mek/share/+server.ts new file mode 100644 index 0000000..122e659 --- /dev/null +++ b/src/routes/api/mek/share/+server.ts @@ -0,0 +1,30 @@ +import { text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { parseSignedRequest } from "$lib/server/modules/crypto"; +import { shareMeksForNewClient } from "$lib/server/services/mek"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request, cookies }) => { + const { userId, clientId } = await authorize(cookies, "activeClient"); + const { targetId, meks } = await parseSignedRequest( + clientId, + await request.json(), + z.object({ + targetId: z.number().int().positive(), + meks: z.array( + z.object({ + version: z.number().int().positive(), + mek: z.string().base64().nonempty(), + }), + ), + }), + ); + + await shareMeksForNewClient( + userId, + targetId, + meks.map(({ version, mek }) => ({ version, encMek: mek })), + ); + return text("MEK shared", { headers: { "Content-Type": "text/plain" } }); +};