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" } }); };