From c9331ae5b765b0b354f24b7719a9abcbcd419412 Mon Sep 17 00:00:00 2001 From: static Date: Fri, 4 Jul 2025 23:26:58 +0900 Subject: [PATCH] =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8=ED=8A=B8?= =?UTF-8?q?=EA=B0=80=20Decryption=20Oracle=EB=A1=9C=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EB=90=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94=20=EC=B7=A8=EC=95=BD?= =?UTF-8?q?=EC=A0=90=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/client.ts | 18 ++++++++++----- src/lib/server/db/session.ts | 14 +++++++----- src/lib/server/schemas/auth.ts | 3 ++- src/lib/server/schemas/client.ts | 3 ++- src/lib/server/services/auth.ts | 12 +++++----- src/lib/server/services/client.ts | 22 +++++++++++++------ src/lib/services/auth.ts | 4 ++-- src/lib/services/key.ts | 4 ++-- src/routes/api/auth/upgradeSession/+server.ts | 4 ++-- .../api/auth/upgradeSession/verify/+server.ts | 4 ++-- src/routes/api/client/register/+server.ts | 4 ++-- .../api/client/register/verify/+server.ts | 4 ++-- 12 files changed, 58 insertions(+), 38 deletions(-) diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index cb873c7..373357a 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -178,7 +178,7 @@ export const registerUserClientChallenge = async ( allowedIp: string, expiresAt: Date, ) => { - await db + const { id } = await db .insertInto("user_client_challenge") .values({ user_id: userId, @@ -187,19 +187,25 @@ export const registerUserClientChallenge = async ( allowed_ip: allowedIp, expires_at: expiresAt, }) - .execute(); + .returning("id") + .executeTakeFirstOrThrow(); + return { id }; }; -export const consumeUserClientChallenge = async (userId: number, answer: string, ip: string) => { +export const consumeUserClientChallenge = async ( + challengeId: number, + userId: number, + ip: string, +) => { const challenge = await db .deleteFrom("user_client_challenge") + .where("id", "=", challengeId) .where("user_id", "=", userId) - .where("answer", "=", answer) .where("allowed_ip", "=", ip) .where("expires_at", ">", new Date()) - .returning("client_id") + .returning(["client_id", "answer"]) .executeTakeFirst(); - return challenge ? { clientId: challenge.client_id } : null; + return challenge ? { clientId: challenge.client_id, answer: challenge.answer } : null; }; export const cleanupExpiredUserClientChallenges = async () => { diff --git a/src/lib/server/db/session.ts b/src/lib/server/db/session.ts index 727f795..653c20c 100644 --- a/src/lib/server/db/session.ts +++ b/src/lib/server/db/session.ts @@ -94,7 +94,7 @@ export const registerSessionUpgradeChallenge = async ( expiresAt: Date, ) => { try { - await db + const { id } = await db .insertInto("session_upgrade_challenge") .values({ session_id: sessionId, @@ -103,7 +103,9 @@ export const registerSessionUpgradeChallenge = async ( allowed_ip: allowedIp, expires_at: expiresAt, }) - .execute(); + .returning("id") + .executeTakeFirstOrThrow(); + return { id }; } catch (e) { if (e instanceof pg.DatabaseError && e.code === "23505") { throw new IntegrityError("Challenge already registered"); @@ -113,19 +115,19 @@ export const registerSessionUpgradeChallenge = async ( }; export const consumeSessionUpgradeChallenge = async ( + challengeId: number, sessionId: string, - answer: string, ip: string, ) => { const challenge = await db .deleteFrom("session_upgrade_challenge") + .where("id", "=", challengeId) .where("session_id", "=", sessionId) - .where("answer", "=", answer) .where("allowed_ip", "=", ip) .where("expires_at", ">", new Date()) - .returning("client_id") + .returning(["client_id", "answer"]) .executeTakeFirst(); - return challenge ? { clientId: challenge.client_id } : null; + return challenge ? { clientId: challenge.client_id, answer: challenge.answer } : null; }; export const cleanupExpiredSessionUpgradeChallenges = async () => { diff --git a/src/lib/server/schemas/auth.ts b/src/lib/server/schemas/auth.ts index e3d6264..e90b209 100644 --- a/src/lib/server/schemas/auth.ts +++ b/src/lib/server/schemas/auth.ts @@ -19,12 +19,13 @@ export const sessionUpgradeRequest = z.object({ export type SessionUpgradeRequest = z.infer; export const sessionUpgradeResponse = z.object({ + id: z.number().int().positive(), challenge: z.string().base64().nonempty(), }); export type SessionUpgradeResponse = z.infer; export const sessionUpgradeVerifyRequest = z.object({ - answer: z.string().base64().nonempty(), + id: z.number().int().positive(), answerSig: z.string().base64().nonempty(), }); export type SessionUpgradeVerifyRequest = z.infer; diff --git a/src/lib/server/schemas/client.ts b/src/lib/server/schemas/client.ts index 53cbb88..df15e39 100644 --- a/src/lib/server/schemas/client.ts +++ b/src/lib/server/schemas/client.ts @@ -17,12 +17,13 @@ export const clientRegisterRequest = z.object({ export type ClientRegisterRequest = z.infer; export const clientRegisterResponse = z.object({ + id: z.number().int().positive(), challenge: z.string().base64().nonempty(), }); export type ClientRegisterResponse = z.infer; export const clientRegisterVerifyRequest = z.object({ - answer: z.string().base64().nonempty(), + id: z.number().int().positive(), answerSig: z.string().base64().nonempty(), }); export type ClientRegisterVerifyRequest = z.infer; diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts index 81f0333..2eb496c 100644 --- a/src/lib/server/services/auth.ts +++ b/src/lib/server/services/auth.ts @@ -81,7 +81,7 @@ export const createSessionUpgradeChallenge = async ( } const { answer, challenge } = await generateChallenge(32, encPubKey); - await registerSessionUpgradeChallenge( + const { id } = await registerSessionUpgradeChallenge( sessionId, client.id, answer.toString("base64"), @@ -89,16 +89,16 @@ export const createSessionUpgradeChallenge = async ( new Date(Date.now() + env.challenge.sessionUpgradeExp), ); - return { challenge: challenge.toString("base64") }; + return { id, challenge: challenge.toString("base64") }; }; export const verifySessionUpgradeChallenge = async ( sessionId: string, ip: string, - answer: string, + challengeId: number, answerSig: string, ) => { - const challenge = await consumeSessionUpgradeChallenge(sessionId, answer, ip); + const challenge = await consumeSessionUpgradeChallenge(challengeId, sessionId, ip); if (!challenge) { error(403, "Invalid challenge answer"); } @@ -106,7 +106,9 @@ export const verifySessionUpgradeChallenge = async ( const client = await getClient(challenge.clientId); if (!client) { error(500, "Invalid challenge answer"); - } else if (!verifySignature(Buffer.from(answer, "base64"), answerSig, client.sigPubKey)) { + } else if ( + !verifySignature(Buffer.from(challenge.answer, "base64"), answerSig, client.sigPubKey) + ) { error(403, "Invalid challenge answer signature"); } diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts index ee1b5b3..811e58c 100644 --- a/src/lib/server/services/client.ts +++ b/src/lib/server/services/client.ts @@ -34,8 +34,14 @@ const createUserClientChallenge = async ( encPubKey: string, ) => { const { answer, challenge } = await generateChallenge(32, encPubKey); - await registerUserClientChallenge(userId, clientId, answer.toString("base64"), ip, expiresAt()); - return challenge.toString("base64"); + const { id } = await registerUserClientChallenge( + userId, + clientId, + answer.toString("base64"), + ip, + expiresAt(), + ); + return { id, challenge: challenge.toString("base64") }; }; export const registerUserClient = async ( @@ -48,7 +54,7 @@ export const registerUserClient = async ( if (client) { try { await createUserClient(userId, client.id); - return { challenge: await createUserClientChallenge(ip, userId, client.id, encPubKey) }; + 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"); @@ -64,7 +70,7 @@ export const registerUserClient = async ( try { const { id: clientId } = await createClient(encPubKey, sigPubKey, userId); - return { challenge: await createUserClientChallenge(ip, userId, clientId, encPubKey) }; + 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"); @@ -77,10 +83,10 @@ export const registerUserClient = async ( export const verifyUserClient = async ( userId: number, ip: string, - answer: string, + challengeId: number, answerSig: string, ) => { - const challenge = await consumeUserClientChallenge(userId, answer, ip); + const challenge = await consumeUserClientChallenge(challengeId, userId, ip); if (!challenge) { error(403, "Invalid challenge answer"); } @@ -88,7 +94,9 @@ export const verifyUserClient = async ( const client = await getClient(challenge.clientId); if (!client) { error(500, "Invalid challenge answer"); - } else if (!verifySignature(Buffer.from(answer, "base64"), answerSig, client.sigPubKey)) { + } else if ( + !verifySignature(Buffer.from(challenge.answer, "base64"), answerSig, client.sigPubKey) + ) { error(403, "Invalid challenge answer signature"); } diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index 498c794..df49e30 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -18,12 +18,12 @@ export const requestSessionUpgrade = async ( }); if (!res.ok) return false; - const { challenge }: SessionUpgradeResponse = await res.json(); + const { id, challenge }: SessionUpgradeResponse = await res.json(); const answer = await decryptChallenge(challenge, decryptKey); const answerSig = await signMessageRSA(answer, signKey); res = await callPostApi("/api/auth/upgradeSession/verify", { - answer: encodeToBase64(answer), + id, answerSig: encodeToBase64(answerSig), }); return res.ok; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index fb368dd..a7e1c08 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -27,12 +27,12 @@ export const requestClientRegistration = async ( }); if (!res.ok) return false; - const { challenge }: ClientRegisterResponse = await res.json(); + 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", { - answer: encodeToBase64(answer), + id, answerSig: encodeToBase64(answerSig), }); return res.ok; diff --git a/src/routes/api/auth/upgradeSession/+server.ts b/src/routes/api/auth/upgradeSession/+server.ts index 760f4c0..fa0b6cf 100644 --- a/src/routes/api/auth/upgradeSession/+server.ts +++ b/src/routes/api/auth/upgradeSession/+server.ts @@ -15,12 +15,12 @@ export const POST: RequestHandler = async ({ locals, request }) => { if (!zodRes.success) error(400, "Invalid request body"); const { encPubKey, sigPubKey } = zodRes.data; - const { challenge } = await createSessionUpgradeChallenge( + const { id, challenge } = await createSessionUpgradeChallenge( sessionId, userId, locals.ip, encPubKey, sigPubKey, ); - return json(sessionUpgradeResponse.parse({ challenge } satisfies SessionUpgradeResponse)); + return json(sessionUpgradeResponse.parse({ id, challenge } satisfies SessionUpgradeResponse)); }; diff --git a/src/routes/api/auth/upgradeSession/verify/+server.ts b/src/routes/api/auth/upgradeSession/verify/+server.ts index 82cb315..2fe4e36 100644 --- a/src/routes/api/auth/upgradeSession/verify/+server.ts +++ b/src/routes/api/auth/upgradeSession/verify/+server.ts @@ -9,8 +9,8 @@ export const POST: RequestHandler = async ({ locals, request }) => { const zodRes = sessionUpgradeVerifyRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); - const { answer, answerSig } = zodRes.data; + const { id, answerSig } = zodRes.data; - await verifySessionUpgradeChallenge(sessionId, locals.ip, answer, answerSig); + await verifySessionUpgradeChallenge(sessionId, locals.ip, id, answerSig); return text("Session upgraded", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/client/register/+server.ts b/src/routes/api/client/register/+server.ts index d6aa4ce..5ac2a53 100644 --- a/src/routes/api/client/register/+server.ts +++ b/src/routes/api/client/register/+server.ts @@ -15,6 +15,6 @@ export const POST: RequestHandler = async ({ locals, request }) => { if (!zodRes.success) error(400, "Invalid request body"); const { encPubKey, sigPubKey } = zodRes.data; - const { challenge } = await registerUserClient(userId, locals.ip, encPubKey, sigPubKey); - return json(clientRegisterResponse.parse({ challenge } satisfies ClientRegisterResponse)); + 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 index 32d5214..5ac9396 100644 --- a/src/routes/api/client/register/verify/+server.ts +++ b/src/routes/api/client/register/verify/+server.ts @@ -9,8 +9,8 @@ export const POST: RequestHandler = async ({ locals, request }) => { const zodRes = clientRegisterVerifyRequest.safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); - const { answer, answerSig } = zodRes.data; + const { id, answerSig } = zodRes.data; - await verifyUserClient(userId, locals.ip, answer, answerSig); + await verifyUserClient(userId, locals.ip, id, answerSig); return text("Client verified", { headers: { "Content-Type": "text/plain" } }); };