From aa4a1a74eab20fc962369ed8bf82b38ea515ab39 Mon Sep 17 00:00:00 2001 From: static Date: Thu, 25 Dec 2025 18:59:41 +0900 Subject: [PATCH] =?UTF-8?q?/api/client=20=EC=95=84=EB=9E=98=EC=9D=98=20End?= =?UTF-8?q?point=EB=93=A4=EC=9D=84=20tRPC=EB=A1=9C=20=EB=A7=88=EC=9D=B4?= =?UTF-8?q?=EA=B7=B8=EB=A0=88=EC=9D=B4=EC=85=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/client.ts | 16 --- src/lib/server/db/index.ts | 10 ++ src/lib/server/db/mek.ts | 13 -- src/lib/server/modules/mek.ts | 6 - src/lib/server/schemas/client.ts | 36 ------ src/lib/server/schemas/index.ts | 1 - src/lib/server/services/client.ts | 116 ------------------ src/lib/services/key.ts | 35 +++--- src/routes/api/client/list/+server.ts | 11 -- src/routes/api/client/register/+server.ts | 20 --- .../api/client/register/verify/+server.ts | 16 --- src/routes/api/client/status/+server.ts | 17 --- src/trpc/client.ts | 2 +- src/trpc/router.server.ts | 3 +- src/trpc/routers/client.ts | 96 +++++++++++++++ src/trpc/routers/index.ts | 1 + 16 files changed, 128 insertions(+), 271 deletions(-) create mode 100644 src/lib/server/db/index.ts delete mode 100644 src/lib/server/schemas/client.ts delete mode 100644 src/lib/server/services/client.ts delete mode 100644 src/routes/api/client/list/+server.ts delete mode 100644 src/routes/api/client/register/+server.ts delete mode 100644 src/routes/api/client/register/verify/+server.ts delete mode 100644 src/routes/api/client/status/+server.ts create mode 100644 src/trpc/routers/client.ts create mode 100644 src/trpc/routers/index.ts diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index 373357a..272df61 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -98,22 +98,6 @@ export const createUserClient = async (userId: number, clientId: number) => { } }; -export const getAllUserClients = async (userId: number) => { - const userClients = await db - .selectFrom("user_client") - .selectAll() - .where("user_id", "=", userId) - .execute(); - return userClients.map( - ({ user_id, client_id, state }) => - ({ - userId: user_id, - clientId: client_id, - state, - }) satisfies UserClient, - ); -}; - export const getUserClient = async (userId: number, clientId: number) => { const userClient = await db .selectFrom("user_client") diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts new file mode 100644 index 0000000..5c21deb --- /dev/null +++ b/src/lib/server/db/index.ts @@ -0,0 +1,10 @@ +export * as CategoryRepo from "./category"; +export * as ClientRepo from "./client"; +export * as FileRepo from "./file"; +export * as HskRepo from "./hsk"; +export * as MediaRepo from "./media"; +export * as MekRepo from "./mek"; +export * as SessionRepo from "./session"; +export * as UserRepo from "./user"; + +export * from "./error"; diff --git a/src/lib/server/db/mek.ts b/src/lib/server/db/mek.ts index d6eecb0..65d00f8 100644 --- a/src/lib/server/db/mek.ts +++ b/src/lib/server/db/mek.ts @@ -60,19 +60,6 @@ export const registerInitialMek = async ( }); }; -export const getInitialMek = async (userId: number) => { - const mek = await db - .selectFrom("master_encryption_key") - .selectAll() - .where("user_id", "=", userId) - .where("version", "=", 1) - .limit(1) - .executeTakeFirst(); - return mek - ? ({ userId: mek.user_id, version: mek.version, state: mek.state } satisfies Mek) - : null; -}; - export const getAllValidClientMeks = async (userId: number, clientId: number) => { const clientMeks = await db .selectFrom("client_master_encryption_key") diff --git a/src/lib/server/modules/mek.ts b/src/lib/server/modules/mek.ts index 1605d75..23623f8 100644 --- a/src/lib/server/modules/mek.ts +++ b/src/lib/server/modules/mek.ts @@ -1,13 +1,7 @@ import { error } from "@sveltejs/kit"; import { getUserClientWithDetails } from "$lib/server/db/client"; -import { getInitialMek } from "$lib/server/db/mek"; import { verifySignature } from "$lib/server/modules/crypto"; -export const isInitialMekNeeded = async (userId: number) => { - const initialMek = await getInitialMek(userId); - return !initialMek; -}; - export const verifyClientEncMekSig = async ( userId: number, clientId: number, diff --git a/src/lib/server/schemas/client.ts b/src/lib/server/schemas/client.ts deleted file mode 100644 index 08a76b7..0000000 --- a/src/lib/server/schemas/client.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { z } from "zod"; - -export const clientListResponse = z.object({ - clients: z.array( - z.object({ - id: z.number().int().positive(), - state: z.enum(["pending", "active"]), - }), - ), -}); -export type ClientListResponse = z.output; - -export const clientRegisterRequest = z.object({ - encPubKey: z.string().base64().nonempty(), - sigPubKey: z.string().base64().nonempty(), -}); -export type ClientRegisterRequest = z.input; - -export const clientRegisterResponse = z.object({ - id: z.number().int().positive(), - challenge: z.string().base64().nonempty(), -}); -export type ClientRegisterResponse = z.output; - -export const clientRegisterVerifyRequest = z.object({ - id: z.number().int().positive(), - answerSig: z.string().base64().nonempty(), -}); -export type ClientRegisterVerifyRequest = z.input; - -export const clientStatusResponse = z.object({ - id: z.number().int().positive(), - state: z.enum(["pending", "active"]), - isInitialMekNeeded: z.boolean(), -}); -export type ClientStatusResponse = z.output; diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index 1fed0d0..b2d4fa5 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -1,6 +1,5 @@ export * from "./auth"; export * from "./category"; -export * from "./client"; export * from "./directory"; export * from "./file"; export * from "./hsk"; diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts deleted file mode 100644 index 811e58c..0000000 --- a/src/lib/server/services/client.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { error } from "@sveltejs/kit"; -import { - createClient, - getClient, - getClientByPubKeys, - createUserClient, - getAllUserClients, - getUserClient, - setUserClientStateToPending, - registerUserClientChallenge, - consumeUserClientChallenge, -} from "$lib/server/db/client"; -import { IntegrityError } from "$lib/server/db/error"; -import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto"; -import { isInitialMekNeeded } from "$lib/server/modules/mek"; -import env from "$lib/server/loadenv"; - -export const getUserClientList = async (userId: number) => { - const userClients = await getAllUserClients(userId); - return { - userClients: userClients.map(({ clientId, state }) => ({ - id: clientId, - state: state as "pending" | "active", - })), - }; -}; - -const expiresAt = () => new Date(Date.now() + env.challenge.userClientExp); - -const createUserClientChallenge = async ( - ip: string, - userId: number, - clientId: number, - encPubKey: string, -) => { - const { answer, challenge } = await generateChallenge(32, encPubKey); - const { id } = await registerUserClientChallenge( - userId, - clientId, - answer.toString("base64"), - ip, - expiresAt(), - ); - return { id, challenge: challenge.toString("base64") }; -}; - -export const registerUserClient = async ( - userId: number, - ip: string, - encPubKey: string, - sigPubKey: string, -) => { - const client = await getClientByPubKeys(encPubKey, sigPubKey); - if (client) { - try { - await createUserClient(userId, client.id); - return await createUserClientChallenge(ip, userId, client.id, encPubKey); - } catch (e) { - if (e instanceof IntegrityError && e.message === "User client already exists") { - error(409, "Client already registered"); - } - throw e; - } - } else { - if (encPubKey === sigPubKey) { - error(400, "Same public keys"); - } else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) { - error(400, "Invalid public key(s)"); - } - - try { - const { id: clientId } = await createClient(encPubKey, sigPubKey, userId); - return await createUserClientChallenge(ip, userId, clientId, encPubKey); - } catch (e) { - if (e instanceof IntegrityError && e.message === "Public key(s) already registered") { - error(409, "Public key(s) already used"); - } - throw e; - } - } -}; - -export const verifyUserClient = async ( - userId: number, - ip: string, - challengeId: number, - answerSig: string, -) => { - const challenge = await consumeUserClientChallenge(challengeId, userId, ip); - if (!challenge) { - error(403, "Invalid challenge answer"); - } - - const client = await getClient(challenge.clientId); - if (!client) { - error(500, "Invalid challenge answer"); - } else if ( - !verifySignature(Buffer.from(challenge.answer, "base64"), answerSig, client.sigPubKey) - ) { - error(403, "Invalid challenge answer signature"); - } - - await setUserClientStateToPending(userId, client.id); -}; - -export const getUserClientStatus = async (userId: number, clientId: number) => { - const userClient = await getUserClient(userId, clientId); - if (!userClient) { - error(500, "Invalid session id"); - } - - return { - state: userClient.state as "pending" | "active", - isInitialMekNeeded: await isInitialMekNeeded(userId), - }; -}; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index cecd241..349197b 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -10,15 +10,13 @@ import { verifyMasterKeyWrapped, } from "$lib/modules/crypto"; import type { - ClientRegisterRequest, - ClientRegisterResponse, - ClientRegisterVerifyRequest, InitialHmacSecretRegisterRequest, MasterKeyListResponse, InitialMasterKeyRegisterRequest, } from "$lib/server/schemas"; import { requestSessionUpgrade } from "$lib/services/auth"; import { masterKeyStore, type ClientKeys } from "$lib/stores"; +import { useTRPC } from "$trpc/client"; export const requestClientRegistration = async ( encryptKeyBase64: string, @@ -26,21 +24,24 @@ export const requestClientRegistration = async ( verifyKeyBase64: string, signKey: CryptoKey, ) => { - let res = await callPostApi("/api/client/register", { - encPubKey: encryptKeyBase64, - sigPubKey: verifyKeyBase64, - }); - if (!res.ok) return false; + const trpc = useTRPC(); - const { id, challenge }: ClientRegisterResponse = await res.json(); - const answer = await decryptChallenge(challenge, decryptKey); - const answerSig = await signMessageRSA(answer, signKey); - - res = await callPostApi("/api/client/register/verify", { - id, - answerSig: encodeToBase64(answerSig), - }); - return res.ok; + try { + const { id, challenge } = await trpc.client.register.mutate({ + encPubKey: encryptKeyBase64, + sigPubKey: verifyKeyBase64, + }); + const answer = await decryptChallenge(challenge, decryptKey); + const answerSig = await signMessageRSA(answer, signKey); + await trpc.client.verify.mutate({ + id, + answerSig: encodeToBase64(answerSig), + }); + return true; + } catch { + // TODO: Error Handling + return false; + } }; export const requestClientRegistrationAndSessionUpgrade = async ( diff --git a/src/routes/api/client/list/+server.ts b/src/routes/api/client/list/+server.ts deleted file mode 100644 index 78193ee..0000000 --- a/src/routes/api/client/list/+server.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { clientListResponse, type ClientListResponse } from "$lib/server/schemas"; -import { getUserClientList } from "$lib/server/services/client"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals }) => { - const { userId } = await authorize(locals, "anyClient"); - const { userClients } = await getUserClientList(userId); - return json(clientListResponse.parse({ clients: userClients } satisfies ClientListResponse)); -}; diff --git a/src/routes/api/client/register/+server.ts b/src/routes/api/client/register/+server.ts deleted file mode 100644 index 5ac2a53..0000000 --- a/src/routes/api/client/register/+server.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { error, json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { - clientRegisterRequest, - clientRegisterResponse, - type ClientRegisterResponse, -} from "$lib/server/schemas"; -import { registerUserClient } from "$lib/server/services/client"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId } = await authorize(locals, "notClient"); - - const zodRes = clientRegisterRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { encPubKey, sigPubKey } = zodRes.data; - - const { id, challenge } = await registerUserClient(userId, locals.ip, encPubKey, sigPubKey); - return json(clientRegisterResponse.parse({ id, challenge } satisfies ClientRegisterResponse)); -}; diff --git a/src/routes/api/client/register/verify/+server.ts b/src/routes/api/client/register/verify/+server.ts deleted file mode 100644 index 5ac9396..0000000 --- a/src/routes/api/client/register/verify/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { error, text } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { clientRegisterVerifyRequest } from "$lib/server/schemas"; -import { verifyUserClient } from "$lib/server/services/client"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ locals, request }) => { - const { userId } = await authorize(locals, "notClient"); - - const zodRes = clientRegisterVerifyRequest.safeParse(await request.json()); - if (!zodRes.success) error(400, "Invalid request body"); - const { id, answerSig } = zodRes.data; - - await verifyUserClient(userId, locals.ip, id, answerSig); - return text("Client verified", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/client/status/+server.ts b/src/routes/api/client/status/+server.ts deleted file mode 100644 index a7ecc82..0000000 --- a/src/routes/api/client/status/+server.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { json } from "@sveltejs/kit"; -import { authorize } from "$lib/server/modules/auth"; -import { clientStatusResponse, type ClientStatusResponse } from "$lib/server/schemas"; -import { getUserClientStatus } from "$lib/server/services/client"; -import type { RequestHandler } from "./$types"; - -export const GET: RequestHandler = async ({ locals }) => { - const { userId, clientId } = await authorize(locals, "anyClient"); - const { state, isInitialMekNeeded } = await getUserClientStatus(userId, clientId); - return json( - clientStatusResponse.parse({ - id: clientId, - state, - isInitialMekNeeded, - } satisfies ClientStatusResponse), - ); -}; diff --git a/src/trpc/client.ts b/src/trpc/client.ts index 0596c63..433a743 100644 --- a/src/trpc/client.ts +++ b/src/trpc/client.ts @@ -14,7 +14,7 @@ const createClient = (fetch: typeof globalThis.fetch) => let browserClient: ReturnType; -export const trpc = (fetch = globalThis.fetch) => { +export const useTRPC = (fetch = globalThis.fetch) => { const client = browserClient ?? createClient(fetch); if (browser) { browserClient ??= client; diff --git a/src/trpc/router.server.ts b/src/trpc/router.server.ts index 35aff92..fdaee15 100644 --- a/src/trpc/router.server.ts +++ b/src/trpc/router.server.ts @@ -1,9 +1,10 @@ import type { RequestEvent } from "@sveltejs/kit"; import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; import { createContext, router } from "./init.server"; +import { clientRouter } from "./routers"; export const appRouter = router({ - // TODO + client: clientRouter, }); export const createCaller = (event: RequestEvent) => appRouter.createCaller(createContext(event)); diff --git a/src/trpc/routers/client.ts b/src/trpc/routers/client.ts new file mode 100644 index 0000000..8add385 --- /dev/null +++ b/src/trpc/routers/client.ts @@ -0,0 +1,96 @@ +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { ClientRepo, IntegrityError } from "$lib/server/db"; +import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto"; +import env from "$lib/server/loadenv"; +import { router, roleProcedure } from "../init.server"; + +const createUserClientChallenge = async ( + ip: string, + userId: number, + clientId: number, + encPubKey: string, +) => { + const { answer, challenge } = await generateChallenge(32, encPubKey); + const { id } = await ClientRepo.registerUserClientChallenge( + userId, + clientId, + answer.toString("base64"), + ip, + new Date(Date.now() + env.challenge.userClientExp), + ); + return { id, challenge: challenge.toString("base64") }; +}; + +const clientRouter = router({ + register: roleProcedure["notClient"] + .input( + z.object({ + encPubKey: z.string().base64().nonempty(), + sigPubKey: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const { userId } = ctx.session; + const { encPubKey, sigPubKey } = input; + const client = await ClientRepo.getClientByPubKeys(encPubKey, sigPubKey); + if (client) { + try { + await ClientRepo.createUserClient(userId, client.id); + return await createUserClientChallenge(ctx.locals.ip, userId, client.id, encPubKey); + } catch (e) { + if (e instanceof IntegrityError && e.message === "User client already exists") { + throw new TRPCError({ code: "CONFLICT", message: "Client already registered" }); + } + throw e; + } + } else { + if (encPubKey === sigPubKey) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Same public keys" }); + } else if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) { + throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid public key(s)" }); + } + + try { + const { id: clientId } = await ClientRepo.createClient(encPubKey, sigPubKey, userId); + return await createUserClientChallenge(ctx.locals.ip, userId, clientId, encPubKey); + } catch (e) { + if (e instanceof IntegrityError && e.message === "Public key(s) already registered") { + throw new TRPCError({ code: "CONFLICT", message: "Public key(s) already used" }); + } + throw e; + } + } + }), + + verify: roleProcedure["notClient"] + .input( + z.object({ + id: z.number().int().positive(), + answerSig: z.string().base64().nonempty(), + }), + ) + .mutation(async ({ ctx, input }) => { + const challenge = await ClientRepo.consumeUserClientChallenge( + input.id, + ctx.session.userId, + ctx.locals.ip, + ); + if (!challenge) { + throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer" }); + } + + const client = await ClientRepo.getClient(challenge.clientId); + if (!client) { + throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Invalid challenge answer" }); + } else if ( + !verifySignature(Buffer.from(challenge.answer, "base64"), input.answerSig, client.sigPubKey) + ) { + throw new TRPCError({ code: "FORBIDDEN", message: "Invalid challenge answer signature" }); + } + + await ClientRepo.setUserClientStateToPending(ctx.session.userId, client.id); + }), +}); + +export default clientRouter; diff --git a/src/trpc/routers/index.ts b/src/trpc/routers/index.ts new file mode 100644 index 0000000..6f13a73 --- /dev/null +++ b/src/trpc/routers/index.ts @@ -0,0 +1 @@ +export { default as clientRouter } from "./client";