From 4f20d2edbfc201b86264c7623d26a477ed60394f Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 01:56:12 +0900 Subject: [PATCH] =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=EC=8B=9C=20=EA=B2=80=EC=A6=9D=ED=82=A4?= =?UTF-8?q?=EB=8F=84=20=EB=93=B1=EB=A1=9D=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD=20(WiP)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/client.ts | 38 +++++++++++-- src/lib/server/db/schema/client.ts | 17 ++++-- src/lib/server/modules/crypto.ts | 34 +++++++++++ src/lib/server/services/client.ts | 69 ++++++++++++++++------- src/routes/api/client/register/+server.ts | 12 +++- src/routes/api/client/verify/+server.ts | 5 +- 6 files changed, 140 insertions(+), 35 deletions(-) create mode 100644 src/lib/server/modules/crypto.ts diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index c31579b..595d665 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -1,10 +1,21 @@ -import { and, eq, gt, lte } from "drizzle-orm"; +import { and, or, eq, gt, lte, count } from "drizzle-orm"; import db from "./drizzle"; import { client, userClient, userClientChallenge } from "./schema"; -export const createClient = async (pubKey: string, userId: number) => { +export const createClient = async (encPubKey: string, sigPubKey: string, userId: number) => { return await db.transaction(async (tx) => { - const insertRes = await tx.insert(client).values({ pubKey }).returning({ id: client.id }); + const clients = await tx + .select() + .from(client) + .where(or(eq(client.encPubKey, sigPubKey), eq(client.sigPubKey, encPubKey))); + if (clients.length > 0) { + throw new Error("Already used public key(s)"); + } + + const insertRes = await tx + .insert(client) + .values({ encPubKey, sigPubKey }) + .returning({ id: client.id }); const { id: clientId } = insertRes[0]!; await tx.insert(userClient).values({ userId, clientId }); @@ -12,11 +23,28 @@ export const createClient = async (pubKey: string, userId: number) => { }); }; -export const getClientByPubKey = async (pubKey: string) => { - const clients = await db.select().from(client).where(eq(client.pubKey, pubKey)).execute(); +export const getClient = async (clientId: number) => { + const clients = await db.select().from(client).where(eq(client.id, clientId)).execute(); return clients[0] ?? null; }; +export const getClientByPubKeys = async (encPubKey: string, sigPubKey: string) => { + const clients = await db + .select() + .from(client) + .where(and(eq(client.encPubKey, encPubKey), eq(client.sigPubKey, sigPubKey))) + .execute(); + return clients[0] ?? null; +}; + +export const countClientByPubKey = async (pubKey: string) => { + const clients = await db + .select({ count: count() }) + .from(client) + .where(or(eq(client.encPubKey, pubKey), eq(client.encPubKey, pubKey))); + return clients[0]?.count ?? 0; +}; + export const createUserClient = async (userId: number, clientId: number) => { await db.insert(userClient).values({ userId, clientId }).execute(); }; diff --git a/src/lib/server/db/schema/client.ts b/src/lib/server/db/schema/client.ts index 75c9fc4..bab3475 100644 --- a/src/lib/server/db/schema/client.ts +++ b/src/lib/server/db/schema/client.ts @@ -1,10 +1,17 @@ -import { sqliteTable, text, integer, primaryKey } from "drizzle-orm/sqlite-core"; +import { sqliteTable, text, integer, primaryKey, unique } from "drizzle-orm/sqlite-core"; import { user } from "./user"; -export const client = sqliteTable("client", { - id: integer("id").primaryKey(), - pubKey: text("public_key").notNull().unique(), // Base64 -}); +export const client = sqliteTable( + "client", + { + id: integer("id").primaryKey(), + encPubKey: text("encryption_public_key").notNull().unique(), // Base64 + sigPubKey: text("signature_public_key").notNull().unique(), // Base64 + }, + (t) => ({ + unq: unique().on(t.encPubKey, t.sigPubKey), + }), +); export const userClient = sqliteTable( "user_client", diff --git a/src/lib/server/modules/crypto.ts b/src/lib/server/modules/crypto.ts new file mode 100644 index 0000000..ccf77be --- /dev/null +++ b/src/lib/server/modules/crypto.ts @@ -0,0 +1,34 @@ +import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto"; +import { promisify } from "util"; + +export const generateRandomBytes = async (length: number) => { + return await promisify(randomBytes)(length); +}; + +const makePubKeyPem = (pubKey: string) => + `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; + +export const verifyPubKey = (pubKey: string) => { + const pubKeyPem = makePubKeyPem(pubKey); + const pubKeyObject = createPublicKey(pubKeyPem); + return ( + pubKeyObject.asymmetricKeyType === "rsa" && + pubKeyObject.asymmetricKeyDetails?.modulusLength === 4096 + ); +}; + +export const encryptAsymmetric = (data: Buffer, encPubKey: string) => { + return publicEncrypt({ key: makePubKeyPem(encPubKey), oaepHash: "sha256" }, data); +}; + +export const verifySignature = (data: string, signature: string, sigPubKey: string) => { + return verify( + "rsa-sha256", + Buffer.from(data, "base64"), + { + key: makePubKeyPem(sigPubKey), + padding: constants.RSA_PKCS1_PSS_PADDING, + }, + Buffer.from(signature, "base64"), + ); +}; diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts index a3b9605..104e7a6 100644 --- a/src/lib/server/services/client.ts +++ b/src/lib/server/services/client.ts @@ -1,17 +1,23 @@ import { error } from "@sveltejs/kit"; -import { randomBytes, publicEncrypt, createPublicKey } from "crypto"; import ms from "ms"; -import { promisify } from "util"; import { createClient, - getClientByPubKey, + getClient, + getClientByPubKeys, + countClientByPubKey, createUserClient, getAllUserClients, getUserClient, + setUserClientStateToPending, createUserClientChallenge, getUserClientChallenge, - setUserClientStateToPending, } from "$lib/server/db/client"; +import { + generateRandomBytes, + verifyPubKey, + encryptAsymmetric, + verifySignature, +} from "$lib/server/modules/crypto"; import { isInitialMekNeeded } from "$lib/server/modules/mek"; import env from "$lib/server/loadenv"; @@ -28,42 +34,53 @@ export const getUserClientList = async (userId: number) => { const expiresIn = ms(env.challenge.pubKeyExp); const expiresAt = () => new Date(Date.now() + expiresIn); -const generateChallenge = async (userId: number, ip: string, clientId: number, pubKey: string) => { - const answer = await promisify(randomBytes)(32); +const generateChallenge = async ( + userId: number, + ip: string, + clientId: number, + encPubKey: string, +) => { + const answer = await generateRandomBytes(32); const answerBase64 = answer.toString("base64"); await createUserClientChallenge(userId, clientId, answerBase64, ip, expiresAt()); - const pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; - const challenge = publicEncrypt({ key: pubKeyPem, oaepHash: "sha256" }, answer); + const challenge = encryptAsymmetric(answer, encPubKey); return challenge.toString("base64"); }; -export const registerUserClient = async (userId: number, ip: string, pubKey: string) => { - const client = await getClientByPubKey(pubKey); +export const registerUserClient = async ( + userId: number, + ip: string, + encPubKey: string, + sigPubKey: string, +) => { let clientId; + const client = await getClientByPubKeys(encPubKey, sigPubKey); if (client) { const userClient = await getUserClient(userId, client.id); if (userClient) { - error(409, "Public key already registered"); + error(409, "Client already registered"); } await createUserClient(userId, client.id); clientId = client.id; } else { - const pubKeyPem = `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; - const pubKeyObject = createPublicKey(pubKeyPem); - if ( - pubKeyObject.asymmetricKeyType !== "rsa" || - pubKeyObject.asymmetricKeyDetails?.modulusLength !== 4096 + if (!verifyPubKey(encPubKey) || !verifyPubKey(sigPubKey)) { + error(400, "Invalid public key(s)"); + } else if (encPubKey === sigPubKey) { + error(400, "Public keys must be different"); + } else if ( + (await countClientByPubKey(encPubKey)) > 0 || + (await countClientByPubKey(sigPubKey)) > 0 ) { - error(400, "Invalid public key"); + error(409, "Public key(s) already registered"); } - clientId = await createClient(pubKey, userId); + clientId = await createClient(encPubKey, sigPubKey, userId); } - return await generateChallenge(userId, ip, clientId, pubKey); + return { challenge: await generateChallenge(userId, ip, clientId, encPubKey) }; }; export const getUserClientStatus = async (userId: number, clientId: number) => { @@ -78,7 +95,12 @@ export const getUserClientStatus = async (userId: number, clientId: number) => { }; }; -export const verifyUserClient = async (userId: number, ip: string, answer: string) => { +export const verifyUserClient = async ( + userId: number, + ip: string, + answer: string, + sigAnswer: string, +) => { const challenge = await getUserClientChallenge(answer, ip); if (!challenge) { error(401, "Invalid challenge answer"); @@ -86,5 +108,12 @@ export const verifyUserClient = async (userId: number, ip: string, answer: strin error(403, "Forbidden"); } + const client = await getClient(challenge.clientId); + if (!client) { + error(500, "Invalid challenge answer"); + } else if (!verifySignature(answer, sigAnswer, client.sigPubKey)) { + error(401, "Invalid challenge answer signature"); + } + await setUserClientStateToPending(userId, challenge.clientId); }; diff --git a/src/routes/api/client/register/+server.ts b/src/routes/api/client/register/+server.ts index 72f34ce..d6c81b0 100644 --- a/src/routes/api/client/register/+server.ts +++ b/src/routes/api/client/register/+server.ts @@ -12,12 +12,18 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress const zodRes = z .object({ - pubKey: z.string().base64().nonempty(), + encPubKey: z.string().base64().nonempty(), + sigPubKey: z.string().base64().nonempty(), }) .safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); - const { pubKey } = zodRes.data; + const { encPubKey, sigPubKey } = zodRes.data; - const challenge = await registerUserClient(userId, getClientAddress(), pubKey.trim()); + const { challenge } = await registerUserClient( + userId, + getClientAddress(), + encPubKey.trim(), + sigPubKey.trim(), + ); return json({ challenge }); }; diff --git a/src/routes/api/client/verify/+server.ts b/src/routes/api/client/verify/+server.ts index 65b99b4..2573cb7 100644 --- a/src/routes/api/client/verify/+server.ts +++ b/src/routes/api/client/verify/+server.ts @@ -13,11 +13,12 @@ export const POST: RequestHandler = async ({ request, cookies, getClientAddress const zodRes = z .object({ answer: z.string().base64().nonempty(), + sigAnswer: z.string().base64().nonempty(), }) .safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); - const { answer } = zodRes.data; + const { answer, sigAnswer } = zodRes.data; - await verifyUserClient(userId, getClientAddress(), answer.trim()); + await verifyUserClient(userId, getClientAddress(), answer.trim(), sigAnswer.trim()); return text("Client verified", { headers: { "Content-Type": "text/plain" } }); };