From 4f20d2edbfc201b86264c7623d26a477ed60394f Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 01:56:12 +0900 Subject: [PATCH 01/11] =?UTF-8?q?=ED=81=B4=EB=9D=BC=EC=9D=B4=EC=96=B8?= =?UTF-8?q?=ED=8A=B8=20=EB=93=B1=EB=A1=9D=EC=8B=9C=20=EA=B2=80=EC=A6=9D?= =?UTF-8?q?=ED=82=A4=EB=8F=84=20=EB=93=B1=EB=A1=9D=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=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" } }); }; From b84d6fd5ad98c9b2a2015463f2b048e13d9ab07b Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 03:01:29 +0900 Subject: [PATCH 02/11] =?UTF-8?q?Token=20Upgrade=EC=8B=9C=20=EC=B1=8C?= =?UTF-8?q?=EB=A6=B0=EC=A7=80=EB=A5=BC=20=EA=B1=B0=EC=B9=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 3 +- src/hooks.server.ts | 6 +- src/lib/server/db/client.ts | 2 +- src/lib/server/db/schema/token.ts | 13 +++ src/lib/server/db/token.ts | 45 ++++++++- src/lib/server/loadenv.ts | 3 +- src/lib/server/modules/crypto.ts | 10 +- src/lib/server/services/auth.ts | 92 +++++++++++++++---- src/lib/server/services/client.ts | 24 ++--- src/routes/api/auth/login/+server.ts | 7 +- src/routes/api/auth/logout/+server.ts | 2 +- src/routes/api/auth/refreshToken/+server.ts | 6 +- src/routes/api/auth/upgradeToken/+server.ts | 29 +++--- .../api/auth/upgradeToken/verify/+server.ts | 35 +++++++ 14 files changed, 208 insertions(+), 69 deletions(-) create mode 100644 src/routes/api/auth/upgradeToken/verify/+server.ts diff --git a/.env.example b/.env.example index 665a966..d0fe7e5 100644 --- a/.env.example +++ b/.env.example @@ -5,4 +5,5 @@ JWT_SECRET= DATABASE_URL= JWT_ACCESS_TOKEN_EXPIRES= JWT_REFRESH_TOKEN_EXPIRES= -PUBKEY_CHALLENGE_EXPIRES= +USER_CLIENT_CHALLENGE_EXPIRES= +TOKEN_UPGRADE_CHALLENGE_EXPIRES= diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 777e492..e237845 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -2,7 +2,10 @@ import { redirect, type ServerInit, type Handle } from "@sveltejs/kit"; import schedule from "node-schedule"; import { cleanupExpiredUserClientChallenges } from "$lib/server/db/client"; import { migrateDB } from "$lib/server/db/drizzle"; -import { cleanupExpiredRefreshTokens } from "$lib/server/db/token"; +import { + cleanupExpiredRefreshTokens, + cleanupExpiredTokenUpgradeChallenges, +} from "$lib/server/db/token"; export const init: ServerInit = () => { migrateDB(); @@ -10,6 +13,7 @@ export const init: ServerInit = () => { schedule.scheduleJob("0 * * * *", () => { cleanupExpiredUserClientChallenges(); cleanupExpiredRefreshTokens(); + cleanupExpiredTokenUpgradeChallenges(); }); }; diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index 595d665..5423949 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -90,7 +90,7 @@ export const setUserClientStateToActive = async (userId: number, clientId: numbe .execute(); }; -export const createUserClientChallenge = async ( +export const registerUserClientChallenge = async ( userId: number, clientId: number, answer: string, diff --git a/src/lib/server/db/schema/token.ts b/src/lib/server/db/schema/token.ts index 7007f77..dcf995d 100644 --- a/src/lib/server/db/schema/token.ts +++ b/src/lib/server/db/schema/token.ts @@ -16,3 +16,16 @@ export const refreshToken = sqliteTable( unq: unique().on(t.userId, t.clientId), }), ); + +export const tokenUpgradeChallenge = sqliteTable("token_upgrade_challenge", { + id: integer("id").primaryKey(), + refreshTokenId: text("refresh_token_id") + .notNull() + .references(() => refreshToken.id), + clientId: integer("client_id") + .notNull() + .references(() => client.id), + answer: text("challenge").notNull().unique(), // Base64 + allowedIp: text("allowed_ip").notNull(), + expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), +}); diff --git a/src/lib/server/db/token.ts b/src/lib/server/db/token.ts index 133b3b4..51499cf 100644 --- a/src/lib/server/db/token.ts +++ b/src/lib/server/db/token.ts @@ -1,9 +1,9 @@ import { SqliteError } from "better-sqlite3"; -import { eq, lte } from "drizzle-orm"; +import { and, eq, gt, lte } from "drizzle-orm"; import ms from "ms"; import env from "$lib/server/loadenv"; import db from "./drizzle"; -import { refreshToken } from "./schema"; +import { refreshToken, tokenUpgradeChallenge } from "./schema"; const expiresIn = ms(env.jwt.refreshExp); const expiresAt = () => new Date(Date.now() + expiresIn); @@ -73,3 +73,44 @@ export const revokeRefreshToken = async (tokenId: string) => { export const cleanupExpiredRefreshTokens = async () => { await db.delete(refreshToken).where(lte(refreshToken.expiresAt, new Date())).execute(); }; + +export const registerTokenUpgradeChallenge = async ( + tokenId: string, + clientId: number, + answer: string, + allowedIp: string, + expiresAt: Date, +) => { + await db + .insert(tokenUpgradeChallenge) + .values({ + refreshTokenId: tokenId, + clientId, + answer, + allowedIp, + expiresAt, + }) + .execute(); +}; + +export const getTokenUpgradeChallenge = async (answer: string, ip: string) => { + const challenges = await db + .select() + .from(tokenUpgradeChallenge) + .where( + and( + eq(tokenUpgradeChallenge.answer, answer), + eq(tokenUpgradeChallenge.allowedIp, ip), + gt(tokenUpgradeChallenge.expiresAt, new Date()), + ), + ) + .execute(); + return challenges[0] ?? null; +}; + +export const cleanupExpiredTokenUpgradeChallenges = async () => { + await db + .delete(tokenUpgradeChallenge) + .where(lte(tokenUpgradeChallenge.expiresAt, new Date())) + .execute(); +}; diff --git a/src/lib/server/loadenv.ts b/src/lib/server/loadenv.ts index c31fb07..58f6e72 100644 --- a/src/lib/server/loadenv.ts +++ b/src/lib/server/loadenv.ts @@ -13,6 +13,7 @@ export default { refreshExp: env.JWT_REFRESH_TOKEN_EXPIRES || "14d", }, challenge: { - pubKeyExp: env.PUBKEY_CHALLENGE_EXPIRES || "5m", + userClientExp: env.USER_CLIENT_CHALLENGE_EXPIRES || "5m", + tokenUpgradeExp: env.TOKEN_UPGRADE_CHALLENGE_EXPIRES || "5m", }, }; diff --git a/src/lib/server/modules/crypto.ts b/src/lib/server/modules/crypto.ts index ccf77be..4b1a176 100644 --- a/src/lib/server/modules/crypto.ts +++ b/src/lib/server/modules/crypto.ts @@ -1,10 +1,6 @@ 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-----`; @@ -32,3 +28,9 @@ export const verifySignature = (data: string, signature: string, sigPubKey: stri Buffer.from(signature, "base64"), ); }; + +export const generateChallenge = async (length: number, encPubKey: string) => { + const answer = await promisify(randomBytes)(length); + const challenge = encryptAsymmetric(answer, encPubKey); + return { answer, challenge }; +}; diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts index 8a9a2c4..d8b3fa1 100644 --- a/src/lib/server/services/auth.ts +++ b/src/lib/server/services/auth.ts @@ -1,16 +1,21 @@ import { error } from "@sveltejs/kit"; import argon2 from "argon2"; +import ms from "ms"; import { v4 as uuidv4 } from "uuid"; -import { getClientByPubKey, getUserClient } from "$lib/server/db/client"; +import { getClient, getClientByPubKeys, getUserClient } from "$lib/server/db/client"; import { getUserByEmail } from "$lib/server/db/user"; +import env from "$lib/server/loadenv"; import { getRefreshToken, registerRefreshToken, rotateRefreshToken, upgradeRefreshToken, revokeRefreshToken, + registerTokenUpgradeChallenge, + getTokenUpgradeChallenge, } from "$lib/server/db/token"; import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth"; +import { verifySignature, generateChallenge } from "$lib/server/modules/crypto"; const verifyPassword = async (hash: string, password: string) => { return await argon2.verify(hash, password); @@ -30,23 +35,15 @@ const issueRefreshToken = async (userId: number, clientId?: number) => { return token; }; -export const login = async (email: string, password: string, pubKey?: string) => { +export const login = async (email: string, password: string) => { const user = await getUserByEmail(email); if (!user || !(await verifyPassword(user.password, password))) { error(401, "Invalid email or password"); } - const client = pubKey ? await getClientByPubKey(pubKey) : undefined; - const userClient = client ? await getUserClient(user.id, client.id) : undefined; - if (client === null) { - error(401, "Invalid public key"); - } else if (client && (!userClient || userClient.state === "challenging")) { - error(401, "Unregistered public key"); - } - return { - accessToken: issueAccessToken(user.id, client?.id), - refreshToken: await issueRefreshToken(user.id, client?.id), + accessToken: issueAccessToken(user.id), + refreshToken: await issueRefreshToken(user.id), }; }; @@ -75,7 +72,7 @@ export const logout = async (refreshToken: string) => { await revokeRefreshToken(jti); }; -export const refreshTokens = async (refreshToken: string) => { +export const refreshToken = async (refreshToken: string) => { const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken); const newJti = uuidv4(); @@ -88,20 +85,75 @@ export const refreshTokens = async (refreshToken: string) => { }; }; -export const upgradeTokens = async (refreshToken: string, pubKey: string) => { +const expiresIn = ms(env.challenge.tokenUpgradeExp); +const expiresAt = () => new Date(Date.now() + expiresIn); + +const createChallenge = async ( + ip: string, + tokenId: string, + clientId: number, + encPubKey: string, +) => { + const { answer, challenge } = await generateChallenge(32, encPubKey); + await registerTokenUpgradeChallenge( + tokenId, + clientId, + answer.toString("base64"), + ip, + expiresAt(), + ); + return challenge.toString("base64"); +}; + +export const createTokenUpgradeChallenge = async ( + refreshToken: string, + ip: string, + encPubKey: string, + sigPubKey: string, +) => { + const { jti, userId, clientId } = await verifyRefreshToken(refreshToken); + if (clientId) { + error(403, "Forbidden"); + } + + const client = await getClientByPubKeys(encPubKey, sigPubKey); + const userClient = client ? await getUserClient(userId, client.id) : undefined; + if (!client) { + error(401, "Invalid public key(s)"); + } else if (!userClient || userClient.state === "challenging") { + error(401, "Unregistered client"); + } + + return { challenge: await createChallenge(ip, jti, client.id, encPubKey) }; +}; + +export const upgradeToken = async ( + refreshToken: string, + ip: string, + answer: string, + sigAnswer: string, +) => { const { jti: oldJti, userId, clientId } = await verifyRefreshToken(refreshToken); if (clientId) { error(403, "Forbidden"); } - const client = await getClientByPubKey(pubKey); - const userClient = client ? await getUserClient(userId, client.id) : undefined; - if (!client) { - error(401, "Invalid public key"); - } else if (client && (!userClient || userClient.state === "challenging")) { - error(401, "Unregistered public key"); + const challenge = await getTokenUpgradeChallenge(answer, ip); + if (!challenge) { + error(401, "Invalid challenge answer"); + } else if (challenge.refreshTokenId !== oldJti) { + 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"); + } + + // TODO: Replay attack prevention + const newJti = uuidv4(); if (!(await upgradeRefreshToken(oldJti, newJti, client.id))) { error(500, "Refresh token not found"); diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts index 104e7a6..c3ad525 100644 --- a/src/lib/server/services/client.ts +++ b/src/lib/server/services/client.ts @@ -9,15 +9,10 @@ import { getAllUserClients, getUserClient, setUserClientStateToPending, - createUserClientChallenge, + registerUserClientChallenge, getUserClientChallenge, } from "$lib/server/db/client"; -import { - generateRandomBytes, - verifyPubKey, - encryptAsymmetric, - verifySignature, -} from "$lib/server/modules/crypto"; +import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto"; import { isInitialMekNeeded } from "$lib/server/modules/mek"; import env from "$lib/server/loadenv"; @@ -31,20 +26,17 @@ export const getUserClientList = async (userId: number) => { }; }; -const expiresIn = ms(env.challenge.pubKeyExp); +const expiresIn = ms(env.challenge.userClientExp); const expiresAt = () => new Date(Date.now() + expiresIn); -const generateChallenge = async ( +const createUserClientChallenge = 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 challenge = encryptAsymmetric(answer, encPubKey); + const { answer, challenge } = await generateChallenge(32, encPubKey); + await registerUserClientChallenge(userId, clientId, answer.toString("base64"), ip, expiresAt()); return challenge.toString("base64"); }; @@ -80,7 +72,7 @@ export const registerUserClient = async ( clientId = await createClient(encPubKey, sigPubKey, userId); } - return { challenge: await generateChallenge(userId, ip, clientId, encPubKey) }; + return { challenge: await createUserClientChallenge(userId, ip, clientId, encPubKey) }; }; export const getUserClientStatus = async (userId: number, clientId: number) => { @@ -115,5 +107,7 @@ export const verifyUserClient = async ( error(401, "Invalid challenge answer signature"); } + // TODO: Replay attack prevention + await setUserClientStateToPending(userId, challenge.clientId); }; diff --git a/src/routes/api/auth/login/+server.ts b/src/routes/api/auth/login/+server.ts index ccd86f5..1e2281f 100644 --- a/src/routes/api/auth/login/+server.ts +++ b/src/routes/api/auth/login/+server.ts @@ -10,14 +10,12 @@ export const POST: RequestHandler = async ({ request, cookies }) => { .object({ email: z.string().email().nonempty(), password: z.string().nonempty(), - pubKey: z.string().base64().nonempty().optional(), }) .safeParse(await request.json()); if (!zodRes.success) error(400, "Invalid request body"); + const { email, password } = zodRes.data; - const { email, password, pubKey } = zodRes.data; - const { accessToken, refreshToken } = await login(email.trim(), password.trim(), pubKey?.trim()); - + const { accessToken, refreshToken } = await login(email.trim(), password.trim()); cookies.set("accessToken", accessToken, { path: "/", maxAge: Math.floor(ms(env.jwt.accessExp) / 1000), @@ -28,5 +26,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => { maxAge: Math.floor(ms(env.jwt.refreshExp) / 1000), sameSite: "strict", }); + return text("Logged in", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/auth/logout/+server.ts b/src/routes/api/auth/logout/+server.ts index a2750c9..ae5dcaf 100644 --- a/src/routes/api/auth/logout/+server.ts +++ b/src/routes/api/auth/logout/+server.ts @@ -7,8 +7,8 @@ export const POST: RequestHandler = async ({ cookies }) => { if (!token) error(401, "Refresh token not found"); await logout(token.trim()); - cookies.delete("accessToken", { path: "/" }); cookies.delete("refreshToken", { path: "/api/auth" }); + return text("Logged out", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/auth/refreshToken/+server.ts b/src/routes/api/auth/refreshToken/+server.ts index d05fc52..54fcd03 100644 --- a/src/routes/api/auth/refreshToken/+server.ts +++ b/src/routes/api/auth/refreshToken/+server.ts @@ -1,13 +1,12 @@ import { error, text } from "@sveltejs/kit"; -import { refreshTokens } from "$lib/server/services/auth"; +import { refreshToken as doRefreshToken } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; export const POST: RequestHandler = async ({ cookies }) => { const token = cookies.get("refreshToken"); if (!token) error(401, "Refresh token not found"); - const { accessToken, refreshToken } = await refreshTokens(token.trim()); - + const { accessToken, refreshToken } = await doRefreshToken(token.trim()); cookies.set("accessToken", accessToken, { path: "/", sameSite: "strict", @@ -16,5 +15,6 @@ export const POST: RequestHandler = async ({ cookies }) => { path: "/api/auth", sameSite: "strict", }); + return text("Token refreshed", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/auth/upgradeToken/+server.ts b/src/routes/api/auth/upgradeToken/+server.ts index 20237e4..46fc5ca 100644 --- a/src/routes/api/auth/upgradeToken/+server.ts +++ b/src/routes/api/auth/upgradeToken/+server.ts @@ -1,29 +1,26 @@ -import { error, text } from "@sveltejs/kit"; +import { error, json } from "@sveltejs/kit"; import { z } from "zod"; -import { upgradeTokens } from "$lib/server/services/auth"; +import { createTokenUpgradeChallenge } from "$lib/server/services/auth"; import type { RequestHandler } from "./$types"; -export const POST: RequestHandler = async ({ request, cookies }) => { +export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { const token = cookies.get("refreshToken"); if (!token) error(401, "Refresh token not found"); 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 { encPubKey, sigPubKey } = zodRes.data; - const { pubKey } = zodRes.data; - const { accessToken, refreshToken } = await upgradeTokens(token.trim(), pubKey.trim()); - - cookies.set("accessToken", accessToken, { - path: "/", - sameSite: "strict", - }); - cookies.set("refreshToken", refreshToken, { - path: "/api/auth", - sameSite: "strict", - }); - return text("Token upgraded", { headers: { "Content-Type": "text/plain" } }); + const { challenge } = await createTokenUpgradeChallenge( + token.trim(), + getClientAddress(), + encPubKey.trim(), + sigPubKey.trim(), + ); + return json({ challenge }); }; diff --git a/src/routes/api/auth/upgradeToken/verify/+server.ts b/src/routes/api/auth/upgradeToken/verify/+server.ts new file mode 100644 index 0000000..f4e291f --- /dev/null +++ b/src/routes/api/auth/upgradeToken/verify/+server.ts @@ -0,0 +1,35 @@ +import { error, text } from "@sveltejs/kit"; +import { z } from "zod"; +import { upgradeToken } from "$lib/server/services/auth"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request, cookies, getClientAddress }) => { + const token = cookies.get("refreshToken"); + if (!token) error(401, "Refresh token not found"); + + 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, sigAnswer } = zodRes.data; + + const { accessToken, refreshToken } = await upgradeToken( + token.trim(), + getClientAddress(), + answer.trim(), + sigAnswer.trim(), + ); + cookies.set("accessToken", accessToken, { + path: "/", + sameSite: "strict", + }); + cookies.set("refreshToken", refreshToken, { + path: "/api/auth", + sameSite: "strict", + }); + + return text("Token upgraded", { headers: { "Content-Type": "text/plain" } }); +}; From a64e85848cab40e2ef487cba79c578ab519143ad Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 03:05:14 +0900 Subject: [PATCH 03/11] =?UTF-8?q?=EC=B1=8C=EB=A6=B0=EC=A7=80=20Reply=20Att?= =?UTF-8?q?ack=20=EB=B0=A9=EC=96=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/client.ts | 9 +++++++++ src/lib/server/db/schema/client.ts | 1 + src/lib/server/db/schema/token.ts | 1 + src/lib/server/db/token.ts | 9 +++++++++ src/lib/server/services/auth.ts | 3 ++- src/lib/server/services/client.ts | 4 ++-- 6 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index 5423949..29f4806 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -118,12 +118,21 @@ export const getUserClientChallenge = async (answer: string, ip: string) => { eq(userClientChallenge.answer, answer), eq(userClientChallenge.allowedIp, ip), gt(userClientChallenge.expiresAt, new Date()), + eq(userClientChallenge.isUsed, false), ), ) .execute(); return challenges[0] ?? null; }; +export const markUserClientChallengeAsUsed = async (id: number) => { + await db + .update(userClientChallenge) + .set({ isUsed: true }) + .where(eq(userClientChallenge.id, id)) + .execute(); +}; + export const cleanupExpiredUserClientChallenges = async () => { await db .delete(userClientChallenge) diff --git a/src/lib/server/db/schema/client.ts b/src/lib/server/db/schema/client.ts index bab3475..7d83435 100644 --- a/src/lib/server/db/schema/client.ts +++ b/src/lib/server/db/schema/client.ts @@ -42,4 +42,5 @@ export const userClientChallenge = sqliteTable("user_client_challenge", { answer: text("challenge").notNull().unique(), // Base64 allowedIp: text("allowed_ip").notNull(), expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + isUsed: integer("is_used", { mode: "boolean" }).notNull().default(false), }); diff --git a/src/lib/server/db/schema/token.ts b/src/lib/server/db/schema/token.ts index dcf995d..72106d7 100644 --- a/src/lib/server/db/schema/token.ts +++ b/src/lib/server/db/schema/token.ts @@ -28,4 +28,5 @@ export const tokenUpgradeChallenge = sqliteTable("token_upgrade_challenge", { answer: text("challenge").notNull().unique(), // Base64 allowedIp: text("allowed_ip").notNull(), expiresAt: integer("expires_at", { mode: "timestamp_ms" }).notNull(), + isUsed: integer("is_used", { mode: "boolean" }).notNull().default(false), }); diff --git a/src/lib/server/db/token.ts b/src/lib/server/db/token.ts index 51499cf..d93527f 100644 --- a/src/lib/server/db/token.ts +++ b/src/lib/server/db/token.ts @@ -102,12 +102,21 @@ export const getTokenUpgradeChallenge = async (answer: string, ip: string) => { eq(tokenUpgradeChallenge.answer, answer), eq(tokenUpgradeChallenge.allowedIp, ip), gt(tokenUpgradeChallenge.expiresAt, new Date()), + eq(tokenUpgradeChallenge.isUsed, false), ), ) .execute(); return challenges[0] ?? null; }; +export const markTokenUpgradeChallengeAsUsed = async (id: number) => { + await db + .update(tokenUpgradeChallenge) + .set({ isUsed: true }) + .where(eq(tokenUpgradeChallenge.id, id)) + .execute(); +}; + export const cleanupExpiredTokenUpgradeChallenges = async () => { await db .delete(tokenUpgradeChallenge) diff --git a/src/lib/server/services/auth.ts b/src/lib/server/services/auth.ts index d8b3fa1..aeaf858 100644 --- a/src/lib/server/services/auth.ts +++ b/src/lib/server/services/auth.ts @@ -13,6 +13,7 @@ import { revokeRefreshToken, registerTokenUpgradeChallenge, getTokenUpgradeChallenge, + markTokenUpgradeChallengeAsUsed, } from "$lib/server/db/token"; import { issueToken, verifyToken, TokenError } from "$lib/server/modules/auth"; import { verifySignature, generateChallenge } from "$lib/server/modules/crypto"; @@ -152,7 +153,7 @@ export const upgradeToken = async ( error(401, "Invalid challenge answer signature"); } - // TODO: Replay attack prevention + await markTokenUpgradeChallengeAsUsed(challenge.id); const newJti = uuidv4(); if (!(await upgradeRefreshToken(oldJti, newJti, client.id))) { diff --git a/src/lib/server/services/client.ts b/src/lib/server/services/client.ts index c3ad525..07729c0 100644 --- a/src/lib/server/services/client.ts +++ b/src/lib/server/services/client.ts @@ -11,6 +11,7 @@ import { setUserClientStateToPending, registerUserClientChallenge, getUserClientChallenge, + markUserClientChallengeAsUsed, } from "$lib/server/db/client"; import { verifyPubKey, verifySignature, generateChallenge } from "$lib/server/modules/crypto"; import { isInitialMekNeeded } from "$lib/server/modules/mek"; @@ -107,7 +108,6 @@ export const verifyUserClient = async ( error(401, "Invalid challenge answer signature"); } - // TODO: Replay attack prevention - + await markUserClientChallengeAsUsed(challenge.id); await setUserClientStateToPending(userId, challenge.clientId); }; From 0ef252913ae0fdf5d62fa0e16c9ca6c683990d04 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 04:18:34 +0900 Subject: [PATCH 04/11] =?UTF-8?q?=EC=95=94=ED=98=B8=20=ED=82=A4=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EA=B2=80=EC=A6=9D=ED=82=A4=EC=99=80=20=EC=84=9C?= =?UTF-8?q?=EB=AA=85=ED=82=A4=EB=A5=BC=20=ED=95=A8=EA=BB=98=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/gotoStateful.ts | 10 +- src/lib/indexedDB.ts | 34 +++--- src/lib/modules/crypto.ts | 27 ++++- src/lib/stores/key.ts | 7 +- .../(fullscreen)/key/export/+page.svelte | 39 +++++-- src/routes/(fullscreen)/key/export/service.ts | 102 ++++++++++++++---- .../(fullscreen)/key/generate/+page.svelte | 13 +-- .../(fullscreen)/key/generate/service.ts | 37 +++++-- src/routes/+layout.svelte | 20 ++-- src/routes/services.ts | 18 ++++ 10 files changed, 225 insertions(+), 82 deletions(-) create mode 100644 src/routes/services.ts diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/hooks/gotoStateful.ts index a6f7bd5..4c6e85a 100644 --- a/src/lib/hooks/gotoStateful.ts +++ b/src/lib/hooks/gotoStateful.ts @@ -4,8 +4,14 @@ type Path = "/key/export"; interface KeyExportState { redirectPath: string; - pubKeyBase64: string; - privKeyBase64: string; + encKeyPair: { + pubKeyBase64: string; + privKeyBase64: string; + }; + sigKeyPair: { + pubKeyBase64: string; + privKeyBase64: string; + }; mekDraft: ArrayBuffer; } diff --git a/src/lib/indexedDB.ts b/src/lib/indexedDB.ts index ff0c29e..41576ea 100644 --- a/src/lib/indexedDB.ts +++ b/src/lib/indexedDB.ts @@ -1,33 +1,31 @@ import { Dexie, type EntityTable } from "dexie"; -interface KeyPair { - type: "publicKey" | "privateKey"; +type RSAKeyUsage = "encrypt" | "decrypt" | "sign" | "verify"; + +interface RSAKey { + usage: RSAKeyUsage; key: CryptoKey; } const keyStore = new Dexie("keyStore") as Dexie & { - keyPair: EntityTable; + rsaKey: EntityTable; }; keyStore.version(1).stores({ - keyPair: "type", + rsaKey: "usage, key", }); -export const getKeyPairFromIndexedDB = async () => { - const pubKey = await keyStore.keyPair.get("publicKey"); - const privKey = await keyStore.keyPair.get("privateKey"); - return { - pubKey: pubKey?.key ?? null, - privKey: privKey?.key ?? null, - }; +export const getRSAKey = async (usage: RSAKeyUsage) => { + const key = await keyStore.rsaKey.get(usage); + return key?.key ?? null; }; -export const storeKeyPairIntoIndexedDB = async (pubKey: CryptoKey, privKey: CryptoKey) => { - if (!pubKey.extractable) throw new Error("Public key must be extractable"); - if (privKey.extractable) throw new Error("Private key must be non-extractable"); +export const storeRSAKey = async (key: CryptoKey, usage: RSAKeyUsage) => { + if ((usage === "encrypt" || usage === "verify") && !key.extractable) { + throw new Error("Public key must be extractable"); + } else if ((usage === "decrypt" || usage === "sign") && key.extractable) { + throw new Error("Private key must be non-extractable"); + } - await keyStore.keyPair.bulkPut([ - { type: "publicKey", key: pubKey }, - { type: "privateKey", key: privKey }, - ]); + await keyStore.rsaKey.put({ usage, key }); }; diff --git a/src/lib/modules/crypto.ts b/src/lib/modules/crypto.ts index d3fe0d2..ad7b6ef 100644 --- a/src/lib/modules/crypto.ts +++ b/src/lib/modules/crypto.ts @@ -8,7 +8,7 @@ export const decodeFromBase64 = (data: string) => { return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer; }; -export const generateRSAKeyPair = async () => { +export const generateRSAEncKeyPair = async () => { const keyPair = await window.crypto.subtle.generateKey( { name: "RSA-OAEP", @@ -22,6 +22,20 @@ export const generateRSAKeyPair = async () => { return keyPair; }; +export const generateRSASigKeyPair = async () => { + const keyPair = await window.crypto.subtle.generateKey( + { + name: "RSA-PSS", + modulusLength: 4096, + publicExponent: new Uint8Array([1, 0, 1]), + hash: "SHA-256", + } satisfies RsaHashedKeyGenParams, + true, + ["sign", "verify"], + ); + return keyPair; +}; + export const makeRSAKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) => { const { format, key: exportedKey } = await exportRSAKey(key, type); return await window.crypto.subtle.importKey( @@ -64,6 +78,17 @@ export const decryptRSACiphertext = async (ciphertext: ArrayBuffer, privateKey: ); }; +export const signRSAMessage = async (message: ArrayBuffer, privateKey: CryptoKey) => { + return await window.crypto.subtle.sign( + { + name: "RSA-PSS", + saltLength: 32, + } satisfies RsaPssParams, + privateKey, + message, + ); +}; + export const generateAESKey = async () => { return await window.crypto.subtle.generateKey( { diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts index 4b806c7..e37d19a 100644 --- a/src/lib/stores/key.ts +++ b/src/lib/stores/key.ts @@ -1,4 +1,9 @@ import { writable } from "svelte/store"; -export const keyPairStore = writable(null); +interface KeyPairs { + encKeyPair: CryptoKeyPair; + sigKeyPair: CryptoKeyPair; +} + +export const keyPairsStore = writable(null); export const mekStore = writable>(new Map()); diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index 267fd18..75245cd 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -3,13 +3,13 @@ import { goto } from "$app/navigation"; import { Button, TextButton } from "$lib/components/buttons"; import { BottomDiv } from "$lib/components/divs"; - import { keyPairStore } from "$lib/stores"; + import { keyPairsStore } from "$lib/stores"; import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte"; import BeforeContinueModal from "./BeforeContinueModal.svelte"; import { - createBlobFromKeyPairBase64, - requestPubKeyRegistration, - storeKeyPairPersistently, + makeKeyPairsSaveable, + requestClientRegistration, + storeKeyPairsPersistently, requestTokenUpgrade, requestInitialMekRegistration, } from "./service"; @@ -22,8 +22,9 @@ let isBeforeContinueBottomSheetOpen = $state(false); const exportKeyPair = () => { - const keyPairBlob = createBlobFromKeyPairBase64(data.pubKeyBase64, data.privKeyBase64); - saveAs(keyPairBlob, "arkvalut-keypair.pem"); + const keyPairsSaveable = makeKeyPairsSaveable(data.encKeyPair, data.sigKeyPair); + const keyPairsBlob = new Blob([JSON.stringify(keyPairsSaveable)], { type: "application/json" }); + saveAs(keyPairsBlob, "arkvalut-key.json"); if (!isBeforeContinueBottomSheetOpen) { setTimeout(() => { @@ -33,7 +34,7 @@ }; const registerPubKey = async () => { - if (!$keyPairStore) { + if (!$keyPairsStore) { throw new Error("Failed to find key pair"); } @@ -41,15 +42,31 @@ isBeforeContinueBottomSheetOpen = false; try { - if (!(await requestPubKeyRegistration(data.pubKeyBase64, $keyPairStore.privateKey))) + if ( + !(await requestClientRegistration( + data.encKeyPair.pubKeyBase64, + $keyPairsStore.encKeyPair.privateKey, + data.sigKeyPair.pubKeyBase64, + $keyPairsStore.sigKeyPair.privateKey, + )) + ) throw new Error("Failed to register public key"); - await storeKeyPairPersistently($keyPairStore); + await storeKeyPairsPersistently($keyPairsStore.encKeyPair, $keyPairsStore.sigKeyPair); - if (!(await requestTokenUpgrade(data.pubKeyBase64))) + if ( + !(await requestTokenUpgrade( + data.encKeyPair.pubKeyBase64, + $keyPairsStore.encKeyPair.privateKey, + data.sigKeyPair.pubKeyBase64, + $keyPairsStore.sigKeyPair.privateKey, + )) + ) throw new Error("Failed to upgrade token"); - if (!(await requestInitialMekRegistration(data.mekDraft, $keyPairStore.publicKey))) + if ( + !(await requestInitialMekRegistration(data.mekDraft, $keyPairsStore.encKeyPair.publicKey)) + ) throw new Error("Failed to register initial MEK"); await goto(data.redirectPath); diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index cd0dd89..88a45e6 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,59 +1,117 @@ import { callAPI } from "$lib/hooks"; -import { storeKeyPairIntoIndexedDB } from "$lib/indexedDB"; +import { storeRSAKey } from "$lib/indexedDB"; import { encodeToBase64, decodeFromBase64, encryptRSAPlaintext, decryptRSACiphertext, + signRSAMessage, } from "$lib/modules/crypto"; -export const createBlobFromKeyPairBase64 = (pubKeyBase64: string, privKeyBase64: string) => { - const pubKeyFormatted = pubKeyBase64.match(/.{1,64}/g)?.join("\n"); - const privKeyFormatted = privKeyBase64.match(/.{1,64}/g)?.join("\n"); - if (!pubKeyFormatted || !privKeyFormatted) { - throw new Error("Failed to format key pair"); - } - - const pubKeyPem = `-----BEGIN RSA PUBLIC KEY-----\n${pubKeyFormatted}\n-----END RSA PUBLIC KEY-----`; - const privKeyPem = `-----BEGIN RSA PRIVATE KEY-----\n${privKeyFormatted}\n-----END RSA PRIVATE KEY-----`; - return new Blob([`${pubKeyPem}\n${privKeyPem}\n`], { type: "text/plain" }); +type ExportedKeyPairs = { + generator: "ArkVault"; + exportedAt: Date; +} & { + version: 1; + encKeyPair: { pubKey: string; privKey: string }; + sigKeyPair: { pubKey: string; privKey: string }; }; -export const requestPubKeyRegistration = async (pubKeyBase64: string, privateKey: CryptoKey) => { +export const makeKeyPairsSaveable = ( + encKeyPair: { pubKeyBase64: string; privKeyBase64: string }, + sigKeyPair: { pubKeyBase64: string; privKeyBase64: string }, +) => { + return { + version: 1, + generator: "ArkVault", + exportedAt: new Date(), + encKeyPair: { + pubKey: encKeyPair.pubKeyBase64, + privKey: encKeyPair.privKeyBase64, + }, + sigKeyPair: { + pubKey: sigKeyPair.pubKeyBase64, + privKey: sigKeyPair.privKeyBase64, + }, + } satisfies ExportedKeyPairs; +}; + +export const requestClientRegistration = async ( + encPubKeyBase64: string, + encPrivKey: CryptoKey, + sigPubKeyBase64: string, + sigPrivKey: CryptoKey, +) => { let res = await callAPI("/api/client/register", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ pubKey: pubKeyBase64 }), + body: JSON.stringify({ + encPubKey: encPubKeyBase64, + sigPubKey: sigPubKeyBase64, + }), }); if (!res.ok) return false; - const data = await res.json(); - const challenge = data.challenge as string; - const answer = await decryptRSACiphertext(decodeFromBase64(challenge), privateKey); + const { challenge } = await res.json(); + const answer = await decryptRSACiphertext(decodeFromBase64(challenge), encPrivKey); + const sigAnswer = await signRSAMessage(answer, sigPrivKey); res = await callAPI("/api/client/verify", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ answer: encodeToBase64(answer) }), + body: JSON.stringify({ + answer: encodeToBase64(answer), + sigAnswer: encodeToBase64(sigAnswer), + }), }); return res.ok; }; -export const storeKeyPairPersistently = async (keyPair: CryptoKeyPair) => { - await storeKeyPairIntoIndexedDB(keyPair.publicKey, keyPair.privateKey); +export const storeKeyPairsPersistently = async ( + encKeyPair: CryptoKeyPair, + sigKeyPair: CryptoKeyPair, +) => { + await storeRSAKey(encKeyPair.publicKey, "encrypt"); + await storeRSAKey(encKeyPair.privateKey, "decrypt"); + await storeRSAKey(sigKeyPair.publicKey, "verify"); + await storeRSAKey(sigKeyPair.privateKey, "sign"); }; -export const requestTokenUpgrade = async (pubKeyBase64: string) => { - const res = await fetch("/api/auth/upgradeToken", { +export const requestTokenUpgrade = async ( + encPubKeyBase64: string, + encPrivKey: CryptoKey, + sigPubKeyBase64: string, + sigPrivKey: CryptoKey, +) => { + let res = await fetch("/api/auth/upgradeToken", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ pubKey: pubKeyBase64 }), + body: JSON.stringify({ + encPubKey: encPubKeyBase64, + sigPubKey: sigPubKeyBase64, + }), + }); + if (!res.ok) return false; + + const { challenge } = await res.json(); + const answer = await decryptRSACiphertext(decodeFromBase64(challenge), encPrivKey); + const sigAnswer = await signRSAMessage(answer, sigPrivKey); + + res = await fetch("/api/auth/upgradeToken/verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + answer: encodeToBase64(answer), + sigAnswer: encodeToBase64(sigAnswer), + }), }); return res.ok; }; diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 01bf7d3..b3b8986 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -3,14 +3,15 @@ import { Button, TextButton } from "$lib/components/buttons"; import { TitleDiv, BottomDiv } from "$lib/components/divs"; import { gotoStateful } from "$lib/hooks"; - import { keyPairStore } from "$lib/stores"; + import { keyPairsStore } from "$lib/stores"; import Order from "./Order.svelte"; - import { generateKeyPair, generateMekDraft } from "./service"; + import { generateKeyPairs, generateMekDraft } from "./service"; import IconKey from "~icons/material-symbols/key"; let { data } = $props(); + // TODO: Update const orders = [ { title: "암호 키는 공개 키와 개인 키로 구성돼요.", @@ -33,19 +34,19 @@ const generate = async () => { // TODO: Loading indicator - const { pubKeyBase64, privKeyBase64 } = await generateKeyPair(); + const { encKeyPair, sigKeyPair } = await generateKeyPairs(); const { mekDraft } = await generateMekDraft(); await gotoStateful("/key/export", { redirectPath: data.redirectPath, - pubKeyBase64, - privKeyBase64, + encKeyPair, + sigKeyPair, mekDraft, }); }; $effect(() => { - if ($keyPairStore) { + if ($keyPairsStore) { goto(data.redirectPath); } }); diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index 8900fd3..8c5e6d2 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -1,26 +1,43 @@ import { encodeToBase64, - generateRSAKeyPair, + generateRSAEncKeyPair, + generateRSASigKeyPair, makeRSAKeyNonextractable, exportRSAKey, generateAESKey, makeAESKeyNonextractable, exportAESKey, } from "$lib/modules/crypto"; -import { keyPairStore, mekStore } from "$lib/stores"; +import { keyPairsStore, mekStore } from "$lib/stores"; -export const generateKeyPair = async () => { - const keyPair = await generateRSAKeyPair(); - const privKeySecured = await makeRSAKeyNonextractable(keyPair.privateKey, "private"); +const exportRSAKeyToBase64 = async (key: CryptoKey, type: "public" | "private") => { + return encodeToBase64((await exportRSAKey(key, type)).key); +}; - keyPairStore.set({ - publicKey: keyPair.publicKey, - privateKey: privKeySecured, +export const generateKeyPairs = async () => { + const encKeyPair = await generateRSAEncKeyPair(); + const sigKeyPair = await generateRSASigKeyPair(); + + keyPairsStore.set({ + encKeyPair: { + publicKey: encKeyPair.publicKey, + privateKey: await makeRSAKeyNonextractable(encKeyPair.privateKey, "private"), + }, + sigKeyPair: { + publicKey: sigKeyPair.publicKey, + privateKey: await makeRSAKeyNonextractable(sigKeyPair.privateKey, "private"), + }, }); return { - pubKeyBase64: encodeToBase64((await exportRSAKey(keyPair.publicKey, "public")).key), - privKeyBase64: encodeToBase64((await exportRSAKey(keyPair.privateKey, "private")).key), + encKeyPair: { + pubKeyBase64: await exportRSAKeyToBase64(encKeyPair.publicKey, "public"), + privKeyBase64: await exportRSAKeyToBase64(encKeyPair.privateKey, "private"), + }, + sigKeyPair: { + pubKeyBase64: await exportRSAKeyToBase64(sigKeyPair.publicKey, "public"), + privKeyBase64: await exportRSAKeyToBase64(sigKeyPair.privateKey, "private"), + }, }; }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 44112c1..f881303 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,21 +1,19 @@ diff --git a/src/routes/services.ts b/src/routes/services.ts new file mode 100644 index 0000000..37dc736 --- /dev/null +++ b/src/routes/services.ts @@ -0,0 +1,18 @@ +import { getRSAKey } from "$lib/indexedDB"; +import { keyPairsStore } from "$lib/stores"; + +export const prepareKeyPairStores = async () => { + const encPubKey = await getRSAKey("encrypt"); + const encPrivKey = await getRSAKey("decrypt"); + const sigPubKey = await getRSAKey("verify"); + const sigPrivKey = await getRSAKey("sign"); + if (encPubKey && encPrivKey && sigPubKey && sigPrivKey) { + keyPairsStore.set({ + encKeyPair: { publicKey: encPubKey, privateKey: encPrivKey }, + sigKeyPair: { publicKey: sigPubKey, privateKey: sigPrivKey }, + }); + return true; + } else { + return false; + } +}; From 08a23b61b2c314e30ff87efddafc4973c7233566 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 04:41:34 +0900 Subject: [PATCH 05/11] =?UTF-8?q?=EC=95=94=ED=98=B8=20=ED=82=A4=EA=B0=80?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=EB=90=9C=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=EC=97=90=EC=84=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=EC=9D=84=20=EC=88=98=ED=96=89=ED=95=98=EB=8A=94=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=EC=9D=84=20=EB=B3=80=EA=B2=BD=EB=90=9C=20API?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 우선 이메일과 비밀번호를 이용해 로그인을 수행한 후, Token Upgrade를 수행하도록 변경했습니다. --- .prettierignore | 3 + src/lib/modules/crypto.ts | 6 +- src/lib/services/auth.ts | 41 ++++++++++ src/lib/services/key.ts | 42 ++++++++++ .../(fullscreen)/auth/login/+page.svelte | 19 +++-- src/routes/(fullscreen)/auth/login/service.ts | 66 ++++++++------- src/routes/(fullscreen)/key/export/service.ts | 81 +------------------ .../(fullscreen)/key/generate/service.ts | 7 +- 8 files changed, 142 insertions(+), 123 deletions(-) create mode 100644 src/lib/services/auth.ts create mode 100644 src/lib/services/key.ts diff --git a/.prettierignore b/.prettierignore index ab78a95..0d5b39a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,3 +2,6 @@ package-lock.json pnpm-lock.yaml yarn.lock + +# Output +/drizzle diff --git a/src/lib/modules/crypto.ts b/src/lib/modules/crypto.ts index ad7b6ef..a5ed99b 100644 --- a/src/lib/modules/crypto.ts +++ b/src/lib/modules/crypto.ts @@ -50,7 +50,7 @@ export const makeRSAKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) ); }; -export const exportRSAKey = async (key: CryptoKey, type: RSAKeyType) => { +const exportRSAKey = async (key: CryptoKey, type: RSAKeyType) => { const format = type === "public" ? ("spki" as const) : ("pkcs8" as const); return { format, @@ -58,6 +58,10 @@ export const exportRSAKey = async (key: CryptoKey, type: RSAKeyType) => { }; }; +export const exportRSAKeyToBase64 = async (key: CryptoKey, type: RSAKeyType) => { + return encodeToBase64((await exportRSAKey(key, type)).key); +}; + export const encryptRSAPlaintext = async (plaintext: ArrayBuffer, publicKey: CryptoKey) => { return await window.crypto.subtle.encrypt( { diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts new file mode 100644 index 0000000..4ce69c3 --- /dev/null +++ b/src/lib/services/auth.ts @@ -0,0 +1,41 @@ +import { + encodeToBase64, + decodeFromBase64, + decryptRSACiphertext, + signRSAMessage, +} from "$lib/modules/crypto"; + +export const requestTokenUpgrade = async ( + encPubKeyBase64: string, + encPrivKey: CryptoKey, + sigPubKeyBase64: string, + sigPrivKey: CryptoKey, +) => { + let res = await fetch("/api/auth/upgradeToken", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + encPubKey: encPubKeyBase64, + sigPubKey: sigPubKeyBase64, + }), + }); + if (!res.ok) return false; + + const { challenge } = await res.json(); + const answer = await decryptRSACiphertext(decodeFromBase64(challenge), encPrivKey); + const sigAnswer = await signRSAMessage(answer, sigPrivKey); + + res = await fetch("/api/auth/upgradeToken/verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + answer: encodeToBase64(answer), + sigAnswer: encodeToBase64(sigAnswer), + }), + }); + return res.ok; +}; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts new file mode 100644 index 0000000..56b96e5 --- /dev/null +++ b/src/lib/services/key.ts @@ -0,0 +1,42 @@ +import { callAPI } from "$lib/hooks"; +import { + encodeToBase64, + decodeFromBase64, + decryptRSACiphertext, + signRSAMessage, +} from "$lib/modules/crypto"; + +export const requestClientRegistration = async ( + encPubKeyBase64: string, + encPrivKey: CryptoKey, + sigPubKeyBase64: string, + sigPrivKey: CryptoKey, +) => { + let res = await callAPI("/api/client/register", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + encPubKey: encPubKeyBase64, + sigPubKey: sigPubKeyBase64, + }), + }); + if (!res.ok) return false; + + const { challenge } = await res.json(); + const answer = await decryptRSACiphertext(decodeFromBase64(challenge), encPrivKey); + const sigAnswer = await signRSAMessage(answer, sigPrivKey); + + res = await callAPI("/api/client/verify", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + answer: encodeToBase64(answer), + sigAnswer: encodeToBase64(sigAnswer), + }), + }); + return res.ok; +}; diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index 015bcfc..fd5390d 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -5,8 +5,8 @@ import { TitleDiv, BottomDiv } from "$lib/components/divs"; import { TextInput } from "$lib/components/inputs"; import { refreshToken } from "$lib/hooks/callAPI"; - import { keyPairStore } from "$lib/stores"; - import { requestLogin } from "./service"; + import { keyPairsStore } from "$lib/stores"; + import { requestLogin, requestTokenUpgrade } from "./service"; let { data } = $props(); @@ -16,14 +16,23 @@ const login = async () => { // TODO: Validation - if (await requestLogin(email, password, $keyPairStore)) { + try { + if (!(await requestLogin(email, password))) throw new Error("Failed to login"); + + if ( + $keyPairsStore && + !(await requestTokenUpgrade($keyPairsStore.encKeyPair, $keyPairsStore.sigKeyPair)) + ) + throw new Error("Failed to upgrade token"); + await goto( - $keyPairStore + $keyPairsStore ? data.redirectPath : "/key/generate?redirect=" + encodeURIComponent(data.redirectPath), ); - } else { + } catch (e) { // TODO: Alert + throw e; } }; diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index d8abe33..0a7f7d9 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,48 +1,46 @@ -import { encodeToBase64, exportRSAKey } from "$lib/modules/crypto"; -import { requestPubKeyRegistration } from "../../key/export/service"; +import { exportRSAKeyToBase64 } from "$lib/modules/crypto"; +import { requestTokenUpgrade as requestTokenUpgradeInternal } from "$lib/services/auth"; +import { requestClientRegistration } from "$lib/services/key"; -const callLoginAPI = async (email: string, password: string, pubKeyBase64?: string) => { - return await fetch("/api/auth/login", { +export const requestLogin = async (email: string, password: string) => { + const res = await fetch("/api/auth/login", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ - email, - password, - pubKey: pubKeyBase64, - }), + body: JSON.stringify({ email, password }), }); + return res.ok; }; -export const requestLogin = async ( - email: string, - password: string, - keyPair: CryptoKeyPair | null, - registerPubKey = true, -): Promise => { - const pubKeyBase64 = keyPair - ? encodeToBase64((await exportRSAKey(keyPair.publicKey, "public")).key) - : undefined; - let loginRes = await callLoginAPI(email, password, pubKeyBase64); - if (loginRes.ok) { +export const requestTokenUpgrade = async (encKeyPair: CryptoKeyPair, sigKeyPair: CryptoKeyPair) => { + const encPubKeyBase64 = await exportRSAKeyToBase64(encKeyPair.publicKey, "public"); + const sigPubKeyBase64 = await exportRSAKeyToBase64(sigKeyPair.publicKey, "public"); + if ( + await requestTokenUpgradeInternal( + encPubKeyBase64, + encKeyPair.privateKey, + sigPubKeyBase64, + sigKeyPair.privateKey, + ) + ) { return true; - } else if (loginRes.status !== 401 || !keyPair || !registerPubKey) { - return false; } - const { message } = await loginRes.json(); - if (message !== "Unregistered public key") { - return false; - } - - loginRes = await callLoginAPI(email, password); - if (!loginRes.ok) { - return false; - } - - if (await requestPubKeyRegistration(pubKeyBase64!, keyPair.privateKey)) { - return requestLogin(email, password, keyPair, false); + if ( + await requestClientRegistration( + encPubKeyBase64, + encKeyPair.privateKey, + sigPubKeyBase64, + sigKeyPair.privateKey, + ) + ) { + return await requestTokenUpgradeInternal( + encPubKeyBase64, + encKeyPair.privateKey, + sigPubKeyBase64, + sigKeyPair.privateKey, + ); } else { return false; } diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index 88a45e6..97cfd8b 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,12 +1,9 @@ import { callAPI } from "$lib/hooks"; import { storeRSAKey } from "$lib/indexedDB"; -import { - encodeToBase64, - decodeFromBase64, - encryptRSAPlaintext, - decryptRSACiphertext, - signRSAMessage, -} from "$lib/modules/crypto"; +import { encodeToBase64, encryptRSAPlaintext } from "$lib/modules/crypto"; + +export { requestTokenUpgrade } from "$lib/services/auth"; +export { requestClientRegistration } from "$lib/services/key"; type ExportedKeyPairs = { generator: "ArkVault"; @@ -36,41 +33,6 @@ export const makeKeyPairsSaveable = ( } satisfies ExportedKeyPairs; }; -export const requestClientRegistration = async ( - encPubKeyBase64: string, - encPrivKey: CryptoKey, - sigPubKeyBase64: string, - sigPrivKey: CryptoKey, -) => { - let res = await callAPI("/api/client/register", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - encPubKey: encPubKeyBase64, - sigPubKey: sigPubKeyBase64, - }), - }); - if (!res.ok) return false; - - const { challenge } = await res.json(); - const answer = await decryptRSACiphertext(decodeFromBase64(challenge), encPrivKey); - const sigAnswer = await signRSAMessage(answer, sigPrivKey); - - res = await callAPI("/api/client/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - answer: encodeToBase64(answer), - sigAnswer: encodeToBase64(sigAnswer), - }), - }); - return res.ok; -}; - export const storeKeyPairsPersistently = async ( encKeyPair: CryptoKeyPair, sigKeyPair: CryptoKeyPair, @@ -81,41 +43,6 @@ export const storeKeyPairsPersistently = async ( await storeRSAKey(sigKeyPair.privateKey, "sign"); }; -export const requestTokenUpgrade = async ( - encPubKeyBase64: string, - encPrivKey: CryptoKey, - sigPubKeyBase64: string, - sigPrivKey: CryptoKey, -) => { - let res = await fetch("/api/auth/upgradeToken", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - encPubKey: encPubKeyBase64, - sigPubKey: sigPubKeyBase64, - }), - }); - if (!res.ok) return false; - - const { challenge } = await res.json(); - const answer = await decryptRSACiphertext(decodeFromBase64(challenge), encPrivKey); - const sigAnswer = await signRSAMessage(answer, sigPrivKey); - - res = await fetch("/api/auth/upgradeToken/verify", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - answer: encodeToBase64(answer), - sigAnswer: encodeToBase64(sigAnswer), - }), - }); - return res.ok; -}; - export const requestInitialMekRegistration = async ( mekDraft: ArrayBuffer, publicKey: CryptoKey, diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index 8c5e6d2..c5aacf7 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -1,19 +1,14 @@ import { - encodeToBase64, generateRSAEncKeyPair, generateRSASigKeyPair, makeRSAKeyNonextractable, - exportRSAKey, + exportRSAKeyToBase64, generateAESKey, makeAESKeyNonextractable, exportAESKey, } from "$lib/modules/crypto"; import { keyPairsStore, mekStore } from "$lib/stores"; -const exportRSAKeyToBase64 = async (key: CryptoKey, type: "public" | "private") => { - return encodeToBase64((await exportRSAKey(key, type)).key); -}; - export const generateKeyPairs = async () => { const encKeyPair = await generateRSAEncKeyPair(); const sigKeyPair = await generateRSASigKeyPair(); From 0f8797504066dc7697cdf66d5424139f78713bdf Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 05:00:03 +0900 Subject: [PATCH 06/11] =?UTF-8?q?Token=20Refresh/Upgrade=EC=99=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=EB=90=9C=20DB=20=EC=A0=9C=EC=95=BD=20?= =?UTF-8?q?=EC=9C=84=EB=B0=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/crypto.ts | 16 ++++++- src/lib/server/db/token.ts | 48 +++++++++++-------- .../(fullscreen)/key/generate/service.ts | 7 +-- 3 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/lib/modules/crypto.ts b/src/lib/modules/crypto.ts index a5ed99b..b188ca8 100644 --- a/src/lib/modules/crypto.ts +++ b/src/lib/modules/crypto.ts @@ -36,7 +36,7 @@ export const generateRSASigKeyPair = async () => { return keyPair; }; -export const makeRSAKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) => { +export const makeRSAEncKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) => { const { format, key: exportedKey } = await exportRSAKey(key, type); return await window.crypto.subtle.importKey( format, @@ -50,6 +50,20 @@ export const makeRSAKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) ); }; +export const makeRSASigKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) => { + const { format, key: exportedKey } = await exportRSAKey(key, type); + return await window.crypto.subtle.importKey( + format, + exportedKey, + { + name: "RSA-PSS", + hash: "SHA-256", + } satisfies RsaHashedImportParams, + false, + [type === "public" ? "verify" : "sign"], + ); +}; + const exportRSAKey = async (key: CryptoKey, type: RSAKeyType) => { const format = type === "public" ? ("spki" as const) : ("pkcs8" as const); return { diff --git a/src/lib/server/db/token.ts b/src/lib/server/db/token.ts index d93527f..61545e4 100644 --- a/src/lib/server/db/token.ts +++ b/src/lib/server/db/token.ts @@ -38,15 +38,20 @@ export const getRefreshToken = async (tokenId: string) => { }; export const rotateRefreshToken = async (oldTokenId: string, newTokenId: string) => { - const res = await db - .update(refreshToken) - .set({ - id: newTokenId, - expiresAt: expiresAt(), - }) - .where(eq(refreshToken.id, oldTokenId)) - .execute(); - return res.changes > 0; + return await db.transaction(async (tx) => { + await tx + .delete(tokenUpgradeChallenge) + .where(eq(tokenUpgradeChallenge.refreshTokenId, oldTokenId)); + const res = await db + .update(refreshToken) + .set({ + id: newTokenId, + expiresAt: expiresAt(), + }) + .where(eq(refreshToken.id, oldTokenId)) + .execute(); + return res.changes > 0; + }); }; export const upgradeRefreshToken = async ( @@ -54,16 +59,21 @@ export const upgradeRefreshToken = async ( newTokenId: string, clientId: number, ) => { - const res = await db - .update(refreshToken) - .set({ - id: newTokenId, - clientId, - expiresAt: expiresAt(), - }) - .where(eq(refreshToken.id, oldTokenId)) - .execute(); - return res.changes > 0; + return await db.transaction(async (tx) => { + await tx + .delete(tokenUpgradeChallenge) + .where(eq(tokenUpgradeChallenge.refreshTokenId, oldTokenId)); + const res = await tx + .update(refreshToken) + .set({ + id: newTokenId, + clientId, + expiresAt: expiresAt(), + }) + .where(eq(refreshToken.id, oldTokenId)) + .execute(); + return res.changes > 0; + }); }; export const revokeRefreshToken = async (tokenId: string) => { diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index c5aacf7..438a861 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -1,7 +1,8 @@ import { generateRSAEncKeyPair, generateRSASigKeyPair, - makeRSAKeyNonextractable, + makeRSAEncKeyNonextractable, + makeRSASigKeyNonextractable, exportRSAKeyToBase64, generateAESKey, makeAESKeyNonextractable, @@ -16,11 +17,11 @@ export const generateKeyPairs = async () => { keyPairsStore.set({ encKeyPair: { publicKey: encKeyPair.publicKey, - privateKey: await makeRSAKeyNonextractable(encKeyPair.privateKey, "private"), + privateKey: await makeRSAEncKeyNonextractable(encKeyPair.privateKey, "private"), }, sigKeyPair: { publicKey: sigKeyPair.publicKey, - privateKey: await makeRSAKeyNonextractable(sigKeyPair.privateKey, "private"), + privateKey: await makeRSASigKeyNonextractable(sigKeyPair.privateKey, "private"), }, }); From 88a8b15990a44e22e9cbed013f61abfe304aec22 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 05:02:27 +0900 Subject: [PATCH 07/11] =?UTF-8?q?DB=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- drizzle/0001_silly_vanisher.sql | 20 ++ drizzle/meta/0001_snapshot.json | 611 ++++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 + 3 files changed, 638 insertions(+) create mode 100644 drizzle/0001_silly_vanisher.sql create mode 100644 drizzle/meta/0001_snapshot.json diff --git a/drizzle/0001_silly_vanisher.sql b/drizzle/0001_silly_vanisher.sql new file mode 100644 index 0000000..32f51e0 --- /dev/null +++ b/drizzle/0001_silly_vanisher.sql @@ -0,0 +1,20 @@ +CREATE TABLE `token_upgrade_challenge` ( + `id` integer PRIMARY KEY NOT NULL, + `refresh_token_id` text NOT NULL, + `client_id` integer NOT NULL, + `challenge` text NOT NULL, + `allowed_ip` text NOT NULL, + `expires_at` integer NOT NULL, + `is_used` integer DEFAULT false NOT NULL, + FOREIGN KEY (`refresh_token_id`) REFERENCES `refresh_token`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`client_id`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +ALTER TABLE `client` RENAME COLUMN `public_key` TO `encryption_public_key`;--> statement-breakpoint +DROP INDEX IF EXISTS `client_public_key_unique`;--> statement-breakpoint +ALTER TABLE `client` ADD `signature_public_key` text NOT NULL;--> statement-breakpoint +ALTER TABLE `user_client_challenge` ADD `is_used` integer DEFAULT false NOT NULL;--> statement-breakpoint +CREATE UNIQUE INDEX `token_upgrade_challenge_challenge_unique` ON `token_upgrade_challenge` (`challenge`);--> statement-breakpoint +CREATE UNIQUE INDEX `client_encryption_public_key_unique` ON `client` (`encryption_public_key`);--> statement-breakpoint +CREATE UNIQUE INDEX `client_signature_public_key_unique` ON `client` (`signature_public_key`);--> statement-breakpoint +CREATE UNIQUE INDEX `client_encryption_public_key_signature_public_key_unique` ON `client` (`encryption_public_key`,`signature_public_key`); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json new file mode 100644 index 0000000..c33f453 --- /dev/null +++ b/drizzle/meta/0001_snapshot.json @@ -0,0 +1,611 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "f5b74176-eb87-436d-8f32-6da01727b564", + "prevId": "64e2c1ed-92bf-44d1-9094-7e3610b3224f", + "tables": { + "client": { + "name": "client", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "encryption_public_key": { + "name": "encryption_public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "signature_public_key": { + "name": "signature_public_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "client_encryption_public_key_unique": { + "name": "client_encryption_public_key_unique", + "columns": [ + "encryption_public_key" + ], + "isUnique": true + }, + "client_signature_public_key_unique": { + "name": "client_signature_public_key_unique", + "columns": [ + "signature_public_key" + ], + "isUnique": true + }, + "client_encryption_public_key_signature_public_key_unique": { + "name": "client_encryption_public_key_signature_public_key_unique", + "columns": [ + "encryption_public_key", + "signature_public_key" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user_client": { + "name": "user_client", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "'challenging'" + } + }, + "indexes": {}, + "foreignKeys": { + "user_client_user_id_user_id_fk": { + "name": "user_client_user_id_user_id_fk", + "tableFrom": "user_client", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_client_client_id_client_id_fk": { + "name": "user_client_client_id_client_id_fk", + "tableFrom": "user_client", + "tableTo": "client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "user_client_user_id_client_id_pk": { + "columns": [ + "client_id", + "user_id" + ], + "name": "user_client_user_id_client_id_pk" + } + }, + "uniqueConstraints": {} + }, + "user_client_challenge": { + "name": "user_client_challenge", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_ip": { + "name": "allowed_ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_used": { + "name": "is_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "user_client_challenge_challenge_unique": { + "name": "user_client_challenge_challenge_unique", + "columns": [ + "challenge" + ], + "isUnique": true + } + }, + "foreignKeys": { + "user_client_challenge_user_id_user_id_fk": { + "name": "user_client_challenge_user_id_user_id_fk", + "tableFrom": "user_client_challenge", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "user_client_challenge_client_id_client_id_fk": { + "name": "user_client_challenge_client_id_client_id_fk", + "tableFrom": "user_client_challenge", + "tableTo": "client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "client_master_encryption_key": { + "name": "client_master_encryption_key", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "master_encryption_key_version": { + "name": "master_encryption_key_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_master_encryption_key": { + "name": "encrypted_master_encryption_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "client_master_encryption_key_user_id_user_id_fk": { + "name": "client_master_encryption_key_user_id_user_id_fk", + "tableFrom": "client_master_encryption_key", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "client_master_encryption_key_client_id_client_id_fk": { + "name": "client_master_encryption_key_client_id_client_id_fk", + "tableFrom": "client_master_encryption_key", + "tableTo": "client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "client_master_encryption_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { + "name": "client_master_encryption_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", + "tableFrom": "client_master_encryption_key", + "tableTo": "master_encryption_key", + "columnsFrom": [ + "user_id", + "master_encryption_key_version" + ], + "columnsTo": [ + "user_id", + "version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "client_master_encryption_key_user_id_client_id_master_encryption_key_version_pk": { + "columns": [ + "client_id", + "master_encryption_key_version", + "user_id" + ], + "name": "client_master_encryption_key_user_id_client_id_master_encryption_key_version_pk" + } + }, + "uniqueConstraints": {} + }, + "master_encryption_key": { + "name": "master_encryption_key", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_by": { + "name": "created_by", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "retired_at": { + "name": "retired_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "master_encryption_key_user_id_user_id_fk": { + "name": "master_encryption_key_user_id_user_id_fk", + "tableFrom": "master_encryption_key", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "master_encryption_key_created_by_client_id_fk": { + "name": "master_encryption_key_created_by_client_id_fk", + "tableFrom": "master_encryption_key", + "tableTo": "client", + "columnsFrom": [ + "created_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "master_encryption_key_user_id_version_pk": { + "columns": [ + "user_id", + "version" + ], + "name": "master_encryption_key_user_id_version_pk" + } + }, + "uniqueConstraints": {} + }, + "refresh_token": { + "name": "refresh_token", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "refresh_token_user_id_client_id_unique": { + "name": "refresh_token_user_id_client_id_unique", + "columns": [ + "user_id", + "client_id" + ], + "isUnique": true + } + }, + "foreignKeys": { + "refresh_token_user_id_user_id_fk": { + "name": "refresh_token_user_id_user_id_fk", + "tableFrom": "refresh_token", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "refresh_token_client_id_client_id_fk": { + "name": "refresh_token_client_id_client_id_fk", + "tableFrom": "refresh_token", + "tableTo": "client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "token_upgrade_challenge": { + "name": "token_upgrade_challenge", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "refresh_token_id": { + "name": "refresh_token_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "client_id": { + "name": "client_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "challenge": { + "name": "challenge", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "allowed_ip": { + "name": "allowed_ip", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_used": { + "name": "is_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + } + }, + "indexes": { + "token_upgrade_challenge_challenge_unique": { + "name": "token_upgrade_challenge_challenge_unique", + "columns": [ + "challenge" + ], + "isUnique": true + } + }, + "foreignKeys": { + "token_upgrade_challenge_refresh_token_id_refresh_token_id_fk": { + "name": "token_upgrade_challenge_refresh_token_id_refresh_token_id_fk", + "tableFrom": "token_upgrade_challenge", + "tableTo": "refresh_token", + "columnsFrom": [ + "refresh_token_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "token_upgrade_challenge_client_id_client_id_fk": { + "name": "token_upgrade_challenge_client_id_client_id_fk", + "tableFrom": "token_upgrade_challenge", + "tableTo": "client", + "columnsFrom": [ + "client_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": { + "\"client\".\"public_key\"": "\"client\".\"encryption_public_key\"" + } + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 70b290a..dc91af8 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1735525637133, "tag": "0000_spicy_morgan_stark", "breakpoints": true + }, + { + "idx": 1, + "version": "6", + "when": 1735588850570, + "tag": "0001_silly_vanisher", + "breakpoints": true } ] } \ No newline at end of file From be70ef1507dfc95bbfdfd472058770c2dadfef42 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 05:22:51 +0900 Subject: [PATCH 08/11] =?UTF-8?q?docker-compose.yaml=20=ED=8C=8C=EC=9D=BC?= =?UTF-8?q?=20=EC=97=85=EB=8D=B0=EC=9D=B4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker-compose.yaml b/docker-compose.yaml index ce7e8aa..5a15630 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -10,7 +10,8 @@ services: - JWT_SECRET=${JWT_SECRET:?} # Required - JWT_ACCESS_TOKEN_EXPIRES - JWT_REFRESH_TOKEN_EXPIRES - - PUBKEY_CHALLENGE_EXPIRES + - USER_CLIENT_CHALLENGE_EXPIRES + - TOKEN_UPGRADE_CHALLENGE_EXPIRES # SvelteKit - ADDRESS_HEADER=${TRUST_PROXY:+X-Forwarded-For} - XFF_DEPTH=${TRUST_PROXY:-} From 214568f2ee6c5f56d5346cf3c5fe6c292578d6bc Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 06:20:23 +0900 Subject: [PATCH 09/11] =?UTF-8?q?=ED=94=84=EB=A1=A0=ED=8A=B8=EC=97=94?= =?UTF-8?q?=EB=93=9C=EC=97=90=EC=84=9C=EC=9D=98=20=EC=95=94=ED=98=B8=20?= =?UTF-8?q?=ED=82=A4=20=EA=B4=80=EB=A0=A8=EB=90=9C=20=EB=B3=80=EC=88=98=20?= =?UTF-8?q?=EC=9D=B4=EB=A6=84=20=EB=A6=AC=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/hooks/gotoStateful.ts | 14 +++--- src/lib/indexedDB.ts | 24 +++++++--- src/lib/services/auth.ts | 16 +++---- src/lib/services/key.ts | 16 +++---- src/lib/stores/key.ts | 10 +++-- .../(fullscreen)/auth/login/+page.svelte | 9 ++-- src/routes/(fullscreen)/auth/login/service.ts | 38 +++++++--------- .../(fullscreen)/key/export/+page.svelte | 45 ++++++++++--------- src/routes/(fullscreen)/key/export/service.ts | 44 +++++++++--------- .../(fullscreen)/key/generate/+page.svelte | 11 +++-- .../(fullscreen)/key/generate/service.ts | 32 +++++-------- src/routes/+layout.svelte | 4 +- src/routes/services.ts | 19 ++++---- 13 files changed, 137 insertions(+), 145 deletions(-) diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/hooks/gotoStateful.ts index 4c6e85a..47f1190 100644 --- a/src/lib/hooks/gotoStateful.ts +++ b/src/lib/hooks/gotoStateful.ts @@ -4,14 +4,12 @@ type Path = "/key/export"; interface KeyExportState { redirectPath: string; - encKeyPair: { - pubKeyBase64: string; - privKeyBase64: string; - }; - sigKeyPair: { - pubKeyBase64: string; - privKeyBase64: string; - }; + + encryptKeyBase64: string; + decryptKeyBase64: string; + signKeyBase64: string; + verifyKeyBase64: string; + mekDraft: ArrayBuffer; } diff --git a/src/lib/indexedDB.ts b/src/lib/indexedDB.ts index 41576ea..1282314 100644 --- a/src/lib/indexedDB.ts +++ b/src/lib/indexedDB.ts @@ -12,7 +12,7 @@ const keyStore = new Dexie("keyStore") as Dexie & { }; keyStore.version(1).stores({ - rsaKey: "usage, key", + rsaKey: "usage", }); export const getRSAKey = async (usage: RSAKeyUsage) => { @@ -21,11 +21,23 @@ export const getRSAKey = async (usage: RSAKeyUsage) => { }; export const storeRSAKey = async (key: CryptoKey, usage: RSAKeyUsage) => { - if ((usage === "encrypt" || usage === "verify") && !key.extractable) { - throw new Error("Public key must be extractable"); - } else if ((usage === "decrypt" || usage === "sign") && key.extractable) { - throw new Error("Private key must be non-extractable"); + switch (usage) { + case "encrypt": + case "verify": + if (key.type !== "public") { + throw new Error("Public key required"); + } else if (!key.extractable) { + throw new Error("Public key must be extractable"); + } + break; + case "decrypt": + case "sign": + if (key.type !== "private") { + throw new Error("Private key required"); + } else if (key.extractable) { + throw new Error("Private key must be non-extractable"); + } + break; } - await keyStore.rsaKey.put({ usage, key }); }; diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index 4ce69c3..865b056 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -6,10 +6,10 @@ import { } from "$lib/modules/crypto"; export const requestTokenUpgrade = async ( - encPubKeyBase64: string, - encPrivKey: CryptoKey, - sigPubKeyBase64: string, - sigPrivKey: CryptoKey, + encryptKeyBase64: string, + decryptKey: CryptoKey, + verifyKeyBase64: string, + signKey: CryptoKey, ) => { let res = await fetch("/api/auth/upgradeToken", { method: "POST", @@ -17,15 +17,15 @@ export const requestTokenUpgrade = async ( "Content-Type": "application/json", }, body: JSON.stringify({ - encPubKey: encPubKeyBase64, - sigPubKey: sigPubKeyBase64, + encPubKey: encryptKeyBase64, + sigPubKey: verifyKeyBase64, }), }); if (!res.ok) return false; const { challenge } = await res.json(); - const answer = await decryptRSACiphertext(decodeFromBase64(challenge), encPrivKey); - const sigAnswer = await signRSAMessage(answer, sigPrivKey); + const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey); + const sigAnswer = await signRSAMessage(answer, signKey); res = await fetch("/api/auth/upgradeToken/verify", { method: "POST", diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index 56b96e5..0183982 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -7,10 +7,10 @@ import { } from "$lib/modules/crypto"; export const requestClientRegistration = async ( - encPubKeyBase64: string, - encPrivKey: CryptoKey, - sigPubKeyBase64: string, - sigPrivKey: CryptoKey, + encryptKeyBase64: string, + decryptKey: CryptoKey, + verifyKeyBase64: string, + signKey: CryptoKey, ) => { let res = await callAPI("/api/client/register", { method: "POST", @@ -18,15 +18,15 @@ export const requestClientRegistration = async ( "Content-Type": "application/json", }, body: JSON.stringify({ - encPubKey: encPubKeyBase64, - sigPubKey: sigPubKeyBase64, + encPubKey: encryptKeyBase64, + sigPubKey: verifyKeyBase64, }), }); if (!res.ok) return false; const { challenge } = await res.json(); - const answer = await decryptRSACiphertext(decodeFromBase64(challenge), encPrivKey); - const sigAnswer = await signRSAMessage(answer, sigPrivKey); + const answer = await decryptRSACiphertext(decodeFromBase64(challenge), decryptKey); + const sigAnswer = await signRSAMessage(answer, signKey); res = await callAPI("/api/client/verify", { method: "POST", diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts index e37d19a..19cff13 100644 --- a/src/lib/stores/key.ts +++ b/src/lib/stores/key.ts @@ -1,9 +1,11 @@ import { writable } from "svelte/store"; -interface KeyPairs { - encKeyPair: CryptoKeyPair; - sigKeyPair: CryptoKeyPair; +export interface ClientKeys { + encryptKey: CryptoKey; + decryptKey: CryptoKey; + signKey: CryptoKey; + verifyKey: CryptoKey; } -export const keyPairsStore = writable(null); +export const clientKeyStore = writable(null); export const mekStore = writable>(new Map()); diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index fd5390d..556911f 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -5,7 +5,7 @@ import { TitleDiv, BottomDiv } from "$lib/components/divs"; import { TextInput } from "$lib/components/inputs"; import { refreshToken } from "$lib/hooks/callAPI"; - import { keyPairsStore } from "$lib/stores"; + import { clientKeyStore } from "$lib/stores"; import { requestLogin, requestTokenUpgrade } from "./service"; let { data } = $props(); @@ -19,14 +19,11 @@ try { if (!(await requestLogin(email, password))) throw new Error("Failed to login"); - if ( - $keyPairsStore && - !(await requestTokenUpgrade($keyPairsStore.encKeyPair, $keyPairsStore.sigKeyPair)) - ) + if ($clientKeyStore && !(await requestTokenUpgrade($clientKeyStore))) throw new Error("Failed to upgrade token"); await goto( - $keyPairsStore + $clientKeyStore ? data.redirectPath : "/key/generate?redirect=" + encodeURIComponent(data.redirectPath), ); diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 0a7f7d9..091ebc8 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,6 +1,7 @@ import { exportRSAKeyToBase64 } from "$lib/modules/crypto"; import { requestTokenUpgrade as requestTokenUpgradeInternal } from "$lib/services/auth"; import { requestClientRegistration } from "$lib/services/key"; +import type { ClientKeys } from "$lib/stores"; export const requestLogin = async (email: string, password: string) => { const res = await fetch("/api/auth/login", { @@ -13,33 +14,24 @@ export const requestLogin = async (email: string, password: string) => { return res.ok; }; -export const requestTokenUpgrade = async (encKeyPair: CryptoKeyPair, sigKeyPair: CryptoKeyPair) => { - const encPubKeyBase64 = await exportRSAKeyToBase64(encKeyPair.publicKey, "public"); - const sigPubKeyBase64 = await exportRSAKeyToBase64(sigKeyPair.publicKey, "public"); - if ( - await requestTokenUpgradeInternal( - encPubKeyBase64, - encKeyPair.privateKey, - sigPubKeyBase64, - sigKeyPair.privateKey, - ) - ) { +export const requestTokenUpgrade = async ({ + encryptKey, + decryptKey, + signKey, + verifyKey, +}: ClientKeys) => { + const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey, "public"); + const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey, "public"); + if (await requestTokenUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) { return true; } - if ( - await requestClientRegistration( - encPubKeyBase64, - encKeyPair.privateKey, - sigPubKeyBase64, - sigKeyPair.privateKey, - ) - ) { + if (await requestClientRegistration(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) { return await requestTokenUpgradeInternal( - encPubKeyBase64, - encKeyPair.privateKey, - sigPubKeyBase64, - sigKeyPair.privateKey, + encryptKeyBase64, + decryptKey, + verifyKeyBase64, + signKey, ); } else { return false; diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index 75245cd..fb1ac30 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -3,13 +3,13 @@ import { goto } from "$app/navigation"; import { Button, TextButton } from "$lib/components/buttons"; import { BottomDiv } from "$lib/components/divs"; - import { keyPairsStore } from "$lib/stores"; + import { clientKeyStore } from "$lib/stores"; import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte"; import BeforeContinueModal from "./BeforeContinueModal.svelte"; import { - makeKeyPairsSaveable, + exportClientKeys, requestClientRegistration, - storeKeyPairsPersistently, + storeClientKeys, requestTokenUpgrade, requestInitialMekRegistration, } from "./service"; @@ -22,9 +22,16 @@ let isBeforeContinueBottomSheetOpen = $state(false); const exportKeyPair = () => { - const keyPairsSaveable = makeKeyPairsSaveable(data.encKeyPair, data.sigKeyPair); - const keyPairsBlob = new Blob([JSON.stringify(keyPairsSaveable)], { type: "application/json" }); - saveAs(keyPairsBlob, "arkvalut-key.json"); + const clientKeysExported = exportClientKeys( + data.encryptKeyBase64, + data.decryptKeyBase64, + data.verifyKeyBase64, + data.signKeyBase64, + ); + const clientKeysBlob = new Blob([JSON.stringify(clientKeysExported)], { + type: "application/json", + }); + saveAs(clientKeysBlob, "arkvalut-clientkey.json"); if (!isBeforeContinueBottomSheetOpen) { setTimeout(() => { @@ -34,7 +41,7 @@ }; const registerPubKey = async () => { - if (!$keyPairsStore) { + if (!$clientKeyStore) { throw new Error("Failed to find key pair"); } @@ -44,29 +51,27 @@ try { if ( !(await requestClientRegistration( - data.encKeyPair.pubKeyBase64, - $keyPairsStore.encKeyPair.privateKey, - data.sigKeyPair.pubKeyBase64, - $keyPairsStore.sigKeyPair.privateKey, + data.encryptKeyBase64, + $clientKeyStore.decryptKey, + data.verifyKeyBase64, + $clientKeyStore.signKey, )) ) - throw new Error("Failed to register public key"); + throw new Error("Failed to register client"); - await storeKeyPairsPersistently($keyPairsStore.encKeyPair, $keyPairsStore.sigKeyPair); + await storeClientKeys($clientKeyStore); if ( !(await requestTokenUpgrade( - data.encKeyPair.pubKeyBase64, - $keyPairsStore.encKeyPair.privateKey, - data.sigKeyPair.pubKeyBase64, - $keyPairsStore.sigKeyPair.privateKey, + data.encryptKeyBase64, + $clientKeyStore.decryptKey, + data.verifyKeyBase64, + $clientKeyStore.signKey, )) ) throw new Error("Failed to upgrade token"); - if ( - !(await requestInitialMekRegistration(data.mekDraft, $keyPairsStore.encKeyPair.publicKey)) - ) + if (!(await requestInitialMekRegistration(data.mekDraft, $clientKeyStore.encryptKey))) throw new Error("Failed to register initial MEK"); await goto(data.redirectPath); diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index 97cfd8b..a7aa707 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,6 +1,7 @@ import { callAPI } from "$lib/hooks"; import { storeRSAKey } from "$lib/indexedDB"; import { encodeToBase64, encryptRSAPlaintext } from "$lib/modules/crypto"; +import type { ClientKeys } from "$lib/stores"; export { requestTokenUpgrade } from "$lib/services/auth"; export { requestClientRegistration } from "$lib/services/key"; @@ -10,44 +11,41 @@ type ExportedKeyPairs = { exportedAt: Date; } & { version: 1; - encKeyPair: { pubKey: string; privKey: string }; - sigKeyPair: { pubKey: string; privKey: string }; + encryptKey: string; + decryptKey: string; + verifyKey: string; + signKey: string; }; -export const makeKeyPairsSaveable = ( - encKeyPair: { pubKeyBase64: string; privKeyBase64: string }, - sigKeyPair: { pubKeyBase64: string; privKeyBase64: string }, +export const exportClientKeys = ( + encryptKeyBase64: string, + decryptKeyBase64: string, + verifyKeyBase64: string, + signKeyBase64: string, ) => { return { version: 1, generator: "ArkVault", exportedAt: new Date(), - encKeyPair: { - pubKey: encKeyPair.pubKeyBase64, - privKey: encKeyPair.privKeyBase64, - }, - sigKeyPair: { - pubKey: sigKeyPair.pubKeyBase64, - privKey: sigKeyPair.privKeyBase64, - }, + encryptKey: encryptKeyBase64, + decryptKey: decryptKeyBase64, + verifyKey: verifyKeyBase64, + signKey: signKeyBase64, } satisfies ExportedKeyPairs; }; -export const storeKeyPairsPersistently = async ( - encKeyPair: CryptoKeyPair, - sigKeyPair: CryptoKeyPair, -) => { - await storeRSAKey(encKeyPair.publicKey, "encrypt"); - await storeRSAKey(encKeyPair.privateKey, "decrypt"); - await storeRSAKey(sigKeyPair.publicKey, "verify"); - await storeRSAKey(sigKeyPair.privateKey, "sign"); +export const storeClientKeys = async (clientKeys: ClientKeys) => { + await storeRSAKey(clientKeys.encryptKey, "encrypt"); + await storeRSAKey(clientKeys.decryptKey, "decrypt"); + await storeRSAKey(clientKeys.signKey, "sign"); + await storeRSAKey(clientKeys.verifyKey, "verify"); }; export const requestInitialMekRegistration = async ( mekDraft: ArrayBuffer, - publicKey: CryptoKey, + encryptKey: CryptoKey, ) => { - const mekDraftEncrypted = await encryptRSAPlaintext(mekDraft, publicKey); + const mekDraftEncrypted = await encryptRSAPlaintext(mekDraft, encryptKey); const res = await callAPI("/api/mek/register/initial", { method: "POST", headers: { diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index b3b8986..4d48fa6 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -3,9 +3,9 @@ import { Button, TextButton } from "$lib/components/buttons"; import { TitleDiv, BottomDiv } from "$lib/components/divs"; import { gotoStateful } from "$lib/hooks"; - import { keyPairsStore } from "$lib/stores"; + import { clientKeyStore } from "$lib/stores"; import Order from "./Order.svelte"; - import { generateKeyPairs, generateMekDraft } from "./service"; + import { generateClientKeys, generateMekDraft } from "./service"; import IconKey from "~icons/material-symbols/key"; @@ -34,19 +34,18 @@ const generate = async () => { // TODO: Loading indicator - const { encKeyPair, sigKeyPair } = await generateKeyPairs(); + const clientKeys = await generateClientKeys(); const { mekDraft } = await generateMekDraft(); await gotoStateful("/key/export", { + ...clientKeys, redirectPath: data.redirectPath, - encKeyPair, - sigKeyPair, mekDraft, }); }; $effect(() => { - if ($keyPairsStore) { + if ($clientKeyStore) { goto(data.redirectPath); } }); diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index 438a861..d7c4ce9 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -8,32 +8,24 @@ import { makeAESKeyNonextractable, exportAESKey, } from "$lib/modules/crypto"; -import { keyPairsStore, mekStore } from "$lib/stores"; +import { clientKeyStore, mekStore } from "$lib/stores"; -export const generateKeyPairs = async () => { +export const generateClientKeys = async () => { const encKeyPair = await generateRSAEncKeyPair(); const sigKeyPair = await generateRSASigKeyPair(); - keyPairsStore.set({ - encKeyPair: { - publicKey: encKeyPair.publicKey, - privateKey: await makeRSAEncKeyNonextractable(encKeyPair.privateKey, "private"), - }, - sigKeyPair: { - publicKey: sigKeyPair.publicKey, - privateKey: await makeRSASigKeyNonextractable(sigKeyPair.privateKey, "private"), - }, + clientKeyStore.set({ + encryptKey: encKeyPair.publicKey, + decryptKey: await makeRSAEncKeyNonextractable(encKeyPair.privateKey, "private"), + signKey: await makeRSASigKeyNonextractable(sigKeyPair.privateKey, "private"), + verifyKey: sigKeyPair.publicKey, }); return { - encKeyPair: { - pubKeyBase64: await exportRSAKeyToBase64(encKeyPair.publicKey, "public"), - privKeyBase64: await exportRSAKeyToBase64(encKeyPair.privateKey, "private"), - }, - sigKeyPair: { - pubKeyBase64: await exportRSAKeyToBase64(sigKeyPair.publicKey, "public"), - privKeyBase64: await exportRSAKeyToBase64(sigKeyPair.privateKey, "private"), - }, + encryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.publicKey, "public"), + decryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.privateKey, "private"), + signKeyBase64: await exportRSAKeyToBase64(sigKeyPair.privateKey, "private"), + verifyKeyBase64: await exportRSAKeyToBase64(sigKeyPair.publicKey, "public"), }; }; @@ -42,7 +34,7 @@ export const generateMekDraft = async () => { const mekSecured = await makeAESKeyNonextractable(mek); mekStore.update((meks) => { - meks.set(meks.size, mekSecured); + meks.set(0, mekSecured); return meks; }); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index f881303..34d3688 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,12 +2,12 @@ import { onMount } from "svelte"; import { goto } from "$app/navigation"; import "../app.css"; - import { prepareKeyPairStores } from "./services"; + import { prepareClientKeyStore } from "./services"; let { children } = $props(); onMount(() => { - prepareKeyPairStores().then(async (ok) => { + prepareClientKeyStore().then(async (ok) => { if (!ok && !["/auth", "/key"].some((path) => location.pathname.startsWith(path))) { await goto( "/key/generate?redirect=" + encodeURIComponent(location.pathname + location.search), diff --git a/src/routes/services.ts b/src/routes/services.ts index 37dc736..bb07c6a 100644 --- a/src/routes/services.ts +++ b/src/routes/services.ts @@ -1,16 +1,13 @@ import { getRSAKey } from "$lib/indexedDB"; -import { keyPairsStore } from "$lib/stores"; +import { clientKeyStore } from "$lib/stores"; -export const prepareKeyPairStores = async () => { - const encPubKey = await getRSAKey("encrypt"); - const encPrivKey = await getRSAKey("decrypt"); - const sigPubKey = await getRSAKey("verify"); - const sigPrivKey = await getRSAKey("sign"); - if (encPubKey && encPrivKey && sigPubKey && sigPrivKey) { - keyPairsStore.set({ - encKeyPair: { publicKey: encPubKey, privateKey: encPrivKey }, - sigKeyPair: { publicKey: sigPubKey, privateKey: sigPrivKey }, - }); +export const prepareClientKeyStore = async () => { + const encryptKey = await getRSAKey("encrypt"); + const decryptKey = await getRSAKey("decrypt"); + const signKey = await getRSAKey("sign"); + const verifyKey = await getRSAKey("verify"); + if (encryptKey && decryptKey && signKey && verifyKey) { + clientKeyStore.set({ encryptKey, decryptKey, signKey, verifyKey }); return true; } else { return false; From f4b9137214939c92232503ceba2314790ac803da Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 06:34:35 +0900 Subject: [PATCH 10/11] =?UTF-8?q?RSA=20=EA=B4=80=EB=A0=A8=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=EC=97=94=EB=93=9C=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/modules/crypto.ts | 64 +++++-------------- src/routes/(fullscreen)/auth/login/service.ts | 4 +- .../(fullscreen)/key/generate/service.ts | 22 +++---- 3 files changed, 27 insertions(+), 63 deletions(-) diff --git a/src/lib/modules/crypto.ts b/src/lib/modules/crypto.ts index b188ca8..0d82085 100644 --- a/src/lib/modules/crypto.ts +++ b/src/lib/modules/crypto.ts @@ -1,3 +1,4 @@ +export type RSAKeyPurpose = "encryption" | "signature"; export type RSAKeyType = "public" | "private"; export const encodeToBase64 = (data: ArrayBuffer) => { @@ -8,72 +9,40 @@ export const decodeFromBase64 = (data: string) => { return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer; }; -export const generateRSAEncKeyPair = async () => { - const keyPair = await window.crypto.subtle.generateKey( +export const generateRSAKeyPair = async (purpose: RSAKeyPurpose) => { + return await window.crypto.subtle.generateKey( { - name: "RSA-OAEP", + name: purpose === "encryption" ? "RSA-OAEP" : "RSA-PSS", modulusLength: 4096, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", } satisfies RsaHashedKeyGenParams, true, - ["encrypt", "decrypt"], + purpose === "encryption" ? ["encrypt", "decrypt"] : ["sign", "verify"], ); - return keyPair; }; -export const generateRSASigKeyPair = async () => { - const keyPair = await window.crypto.subtle.generateKey( - { - name: "RSA-PSS", - modulusLength: 4096, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - } satisfies RsaHashedKeyGenParams, - true, - ["sign", "verify"], - ); - return keyPair; -}; - -export const makeRSAEncKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) => { - const { format, key: exportedKey } = await exportRSAKey(key, type); +export const makeRSAKeyNonextractable = async (key: CryptoKey) => { + const { format, key: exportedKey } = await exportRSAKey(key); return await window.crypto.subtle.importKey( format, exportedKey, - { - name: "RSA-OAEP", - hash: "SHA-256", - } satisfies RsaHashedImportParams, + key.algorithm, false, - [type === "public" ? "encrypt" : "decrypt"], + key.usages, ); }; -export const makeRSASigKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) => { - const { format, key: exportedKey } = await exportRSAKey(key, type); - return await window.crypto.subtle.importKey( - format, - exportedKey, - { - name: "RSA-PSS", - hash: "SHA-256", - } satisfies RsaHashedImportParams, - false, - [type === "public" ? "verify" : "sign"], - ); -}; - -const exportRSAKey = async (key: CryptoKey, type: RSAKeyType) => { - const format = type === "public" ? ("spki" as const) : ("pkcs8" as const); +export const exportRSAKey = async (key: CryptoKey) => { + const format = key.type === "public" ? ("spki" as const) : ("pkcs8" as const); return { format, key: await window.crypto.subtle.exportKey(format, key), }; }; -export const exportRSAKeyToBase64 = async (key: CryptoKey, type: RSAKeyType) => { - return encodeToBase64((await exportRSAKey(key, type)).key); +export const exportRSAKeyToBase64 = async (key: CryptoKey) => { + return encodeToBase64((await exportRSAKey(key)).key); }; export const encryptRSAPlaintext = async (plaintext: ArrayBuffer, publicKey: CryptoKey) => { @@ -122,12 +91,9 @@ export const makeAESKeyNonextractable = async (key: CryptoKey) => { return await window.crypto.subtle.importKey( "raw", await exportAESKey(key), - { - name: "AES-GCM", - length: 256, - } satisfies AesKeyAlgorithm, + key.algorithm, false, - ["encrypt", "decrypt"], + key.usages, ); }; diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 091ebc8..77c4620 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -20,8 +20,8 @@ export const requestTokenUpgrade = async ({ signKey, verifyKey, }: ClientKeys) => { - const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey, "public"); - const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey, "public"); + const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey); + const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey); if (await requestTokenUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) { return true; } diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index d7c4ce9..4e16e63 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -1,8 +1,6 @@ import { - generateRSAEncKeyPair, - generateRSASigKeyPair, - makeRSAEncKeyNonextractable, - makeRSASigKeyNonextractable, + generateRSAKeyPair, + makeRSAKeyNonextractable, exportRSAKeyToBase64, generateAESKey, makeAESKeyNonextractable, @@ -11,21 +9,21 @@ import { import { clientKeyStore, mekStore } from "$lib/stores"; export const generateClientKeys = async () => { - const encKeyPair = await generateRSAEncKeyPair(); - const sigKeyPair = await generateRSASigKeyPair(); + const encKeyPair = await generateRSAKeyPair("encryption"); + const sigKeyPair = await generateRSAKeyPair("signature"); clientKeyStore.set({ encryptKey: encKeyPair.publicKey, - decryptKey: await makeRSAEncKeyNonextractable(encKeyPair.privateKey, "private"), - signKey: await makeRSASigKeyNonextractable(sigKeyPair.privateKey, "private"), + decryptKey: await makeRSAKeyNonextractable(encKeyPair.privateKey), + signKey: await makeRSAKeyNonextractable(sigKeyPair.privateKey), verifyKey: sigKeyPair.publicKey, }); return { - encryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.publicKey, "public"), - decryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.privateKey, "private"), - signKeyBase64: await exportRSAKeyToBase64(sigKeyPair.privateKey, "private"), - verifyKeyBase64: await exportRSAKeyToBase64(sigKeyPair.publicKey, "public"), + encryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.publicKey), + decryptKeyBase64: await exportRSAKeyToBase64(encKeyPair.privateKey), + signKeyBase64: await exportRSAKeyToBase64(sigKeyPair.privateKey), + verifyKeyBase64: await exportRSAKeyToBase64(sigKeyPair.publicKey), }; }; From 5034598d0b2520fcf2c50c8cd20297176ed2dbda Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 06:40:31 +0900 Subject: [PATCH 11/11] =?UTF-8?q?=EC=82=AC=EC=86=8C=ED=95=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/modules/crypto.ts | 8 ++++---- src/routes/(fullscreen)/key/export/+page.svelte | 2 +- src/routes/(fullscreen)/key/export/service.ts | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/lib/server/modules/crypto.ts b/src/lib/server/modules/crypto.ts index 4b1a176..49201d9 100644 --- a/src/lib/server/modules/crypto.ts +++ b/src/lib/server/modules/crypto.ts @@ -1,11 +1,11 @@ import { constants, randomBytes, createPublicKey, publicEncrypt, verify } from "crypto"; import { promisify } from "util"; -const makePubKeyPem = (pubKey: string) => +const makePubKeyToPem = (pubKey: string) => `-----BEGIN PUBLIC KEY-----\n${pubKey}\n-----END PUBLIC KEY-----`; export const verifyPubKey = (pubKey: string) => { - const pubKeyPem = makePubKeyPem(pubKey); + const pubKeyPem = makePubKeyToPem(pubKey); const pubKeyObject = createPublicKey(pubKeyPem); return ( pubKeyObject.asymmetricKeyType === "rsa" && @@ -14,7 +14,7 @@ export const verifyPubKey = (pubKey: string) => { }; export const encryptAsymmetric = (data: Buffer, encPubKey: string) => { - return publicEncrypt({ key: makePubKeyPem(encPubKey), oaepHash: "sha256" }, data); + return publicEncrypt({ key: makePubKeyToPem(encPubKey), oaepHash: "sha256" }, data); }; export const verifySignature = (data: string, signature: string, sigPubKey: string) => { @@ -22,7 +22,7 @@ export const verifySignature = (data: string, signature: string, sigPubKey: stri "rsa-sha256", Buffer.from(data, "base64"), { - key: makePubKeyPem(sigPubKey), + key: makePubKeyToPem(sigPubKey), padding: constants.RSA_PKCS1_PSS_PADDING, }, Buffer.from(signature, "base64"), diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index fb1ac30..74ee5ea 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -25,8 +25,8 @@ const clientKeysExported = exportClientKeys( data.encryptKeyBase64, data.decryptKeyBase64, - data.verifyKeyBase64, data.signKeyBase64, + data.verifyKeyBase64, ); const clientKeysBlob = new Blob([JSON.stringify(clientKeysExported)], { type: "application/json", diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index a7aa707..bd9493c 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -13,15 +13,15 @@ type ExportedKeyPairs = { version: 1; encryptKey: string; decryptKey: string; - verifyKey: string; signKey: string; + verifyKey: string; }; export const exportClientKeys = ( encryptKeyBase64: string, decryptKeyBase64: string, - verifyKeyBase64: string, signKeyBase64: string, + verifyKeyBase64: string, ) => { return { version: 1, @@ -29,8 +29,8 @@ export const exportClientKeys = ( exportedAt: new Date(), encryptKey: encryptKeyBase64, decryptKey: decryptKeyBase64, - verifyKey: verifyKeyBase64, signKey: signKeyBase64, + verifyKey: verifyKeyBase64, } satisfies ExportedKeyPairs; };