diff --git a/src/lib/modules/crypto.ts b/src/lib/modules/crypto.ts index 8a476d1..da57744 100644 --- a/src/lib/modules/crypto.ts +++ b/src/lib/modules/crypto.ts @@ -76,6 +76,22 @@ export const signRSAMessage = async (message: BufferSource, privateKey: CryptoKe ); }; +export const verifyRSASignature = async ( + message: BufferSource, + signature: BufferSource, + publicKey: CryptoKey, +) => { + return await window.crypto.subtle.verify( + { + name: "RSA-PSS", + saltLength: 32, + } satisfies RsaPssParams, + publicKey, + signature, + message, + ); +}; + export const generateAESKey = async () => { return await window.crypto.subtle.generateKey( { @@ -136,3 +152,24 @@ export const signRequest = async (data: T, privateKey: CryptoKey) => { signature: encodeToBase64(signature), }); }; + +export const signMasterKeyWrapped = async ( + version: number, + masterKeyWrapped: ArrayBuffer, + privateKey: CryptoKey, +) => { + const data = JSON.stringify({ version, key: encodeToBase64(masterKeyWrapped) }); + const dataBuffer = new TextEncoder().encode(data); + return encodeToBase64(await signRSAMessage(dataBuffer, privateKey)); +}; + +export const verifyMasterKeyWrappedSig = async ( + version: number, + masterKeyWrappedBase64: string, + masterKeyWrappedSig: string, + publicKey: CryptoKey, +) => { + const data = JSON.stringify({ version, key: masterKeyWrappedBase64 }); + const dataBuffer = new TextEncoder().encode(data); + return await verifyRSASignature(dataBuffer, decodeFromBase64(masterKeyWrappedSig), publicKey); +}; diff --git a/src/lib/server/db/mek.ts b/src/lib/server/db/mek.ts index 3c0bfff..94e6553 100644 --- a/src/lib/server/db/mek.ts +++ b/src/lib/server/db/mek.ts @@ -1,8 +1,13 @@ -import { and, or, eq, lt, desc } from "drizzle-orm"; +import { and, or, eq } from "drizzle-orm"; import db from "./drizzle"; -import { mek, clientMek, userClient } from "./schema"; +import { mek, clientMek } from "./schema"; -export const registerInitialMek = async (userId: number, createdBy: number, encMek: string) => { +export const registerInitialMek = async ( + userId: number, + createdBy: number, + encMek: string, + encMekSig: string, +) => { await db.transaction(async (tx) => { await tx.insert(mek).values({ userId, @@ -16,70 +21,11 @@ export const registerInitialMek = async (userId: number, createdBy: number, encM clientId: createdBy, mekVersion: 1, encMek, + encMekSig, }); }); }; -export const registerActiveMek = async ( - userId: number, - version: number, - createdBy: number, - clientMeks: { - clientId: number; - encMek: string; - }[], -) => { - await db.transaction(async (tx) => { - // 1. Check if the clientMeks are valid - const userClients = await tx - .select() - .from(userClient) - .where(and(eq(userClient.userId, userId), eq(userClient.state, "active"))); - if ( - clientMeks.length !== userClients.length || - !clientMeks.every((clientMek) => - userClients.some((userClient) => userClient.clientId === clientMek.clientId), - ) - ) { - throw new Error("Invalid key list"); - } - - // 2. Retire the old active MEK and insert the new one - await tx - .update(mek) - .set({ - state: "retired", - retiredAt: new Date(), - }) - .where(and(eq(mek.userId, userId), lt(mek.version, version), eq(mek.state, "active"))); - await tx.insert(mek).values({ - userId, - version, - createdBy, - createdAt: new Date(), - state: "active", - }); - - // 3. Insert the new client MEKs - await tx.insert(clientMek).values( - clientMeks.map(({ clientId, encMek }) => ({ - userId, - clientId, - mekVersion: version, - encMek, - })), - ); - }); -}; - -export const getAllValidMeks = async (userId: number) => { - return await db - .select() - .from(mek) - .where(and(eq(mek.userId, userId), or(eq(mek.state, "active"), eq(mek.state, "retired")))) - .execute(); -}; - export const getInitialMek = async (userId: number) => { const meks = await db .select() @@ -89,68 +35,6 @@ export const getInitialMek = async (userId: number) => { return meks[0] ?? null; }; -export const getNextActiveMekVersion = async (userId: number) => { - const meks = await db - .select({ version: mek.version }) - .from(mek) - .where(eq(mek.userId, userId)) - .orderBy(desc(mek.version)) - .limit(1) - .execute(); - if (!meks[0]) { - throw new Error("No MEK found"); - } - return meks[0].version + 1; -}; - -export const registerClientMeks = async ( - userId: number, - clientId: number, - clientMeks: { - version: number; - encMek: string; - }[], -) => { - await db.transaction(async (tx) => { - // 1. Check if the client is valid - const userClients = await tx - .select() - .from(userClient) - .where( - and( - eq(userClient.userId, userId), - eq(userClient.clientId, clientId), - eq(userClient.state, "active"), - ), - ); - if (userClients.length === 0) { - throw new Error("Invalid client"); - } - - // 2. Check if the clientMeks are valid - const meks = await tx - .select() - .from(mek) - .where(and(eq(mek.userId, userId), or(eq(mek.state, "active"), eq(mek.state, "retired")))); - if ( - clientMeks.length !== meks.length || - !clientMeks.every((clientMek) => meks.some((mek) => mek.version === clientMek.version)) - ) { - throw new Error("Invalid key list"); - } - - // 3. Insert the client MEKs - await tx.insert(clientMek).values( - clientMeks.map(({ version, encMek }) => ({ - userId, - clientId, - mekVersion: version, - encMek, - })), - ); - }); -}; - export const getAllValidClientMeks = async (userId: number, clientId: number) => { return await db .select() diff --git a/src/lib/server/db/schema/mek.ts b/src/lib/server/db/schema/mek.ts index f3f8b6b..8ab6fb8 100644 --- a/src/lib/server/db/schema/mek.ts +++ b/src/lib/server/db/schema/mek.ts @@ -30,8 +30,9 @@ export const clientMek = sqliteTable( clientId: integer("client_id") .notNull() .references(() => client.id), - mekVersion: integer("master_encryption_key_version").notNull(), - encMek: text("encrypted_master_encryption_key").notNull(), + mekVersion: integer("version").notNull(), + encMek: text("encrypted_key").notNull(), // Base64 + encMekSig: text("encrypted_key_signature").notNull(), // Base64 }, (t) => ({ pk: primaryKey({ columns: [t.userId, t.clientId, t.mekVersion] }), diff --git a/src/lib/server/modules/mek.ts b/src/lib/server/modules/mek.ts index 940774a..0019ce0 100644 --- a/src/lib/server/modules/mek.ts +++ b/src/lib/server/modules/mek.ts @@ -1,6 +1,25 @@ +import { error } from "@sveltejs/kit"; +import { getUserClientWithDetails } from "$lib/server/db/client"; import { getInitialMek } from "$lib/server/db/mek"; +import { verifySignature } from "$lib/server/modules/crypto"; export const isInitialMekNeeded = async (userId: number) => { const initialMek = await getInitialMek(userId); return !initialMek; }; + +export const verifyClientEncMekSig = async ( + userId: number, + clientId: number, + version: number, + encMek: string, + encMekSig: string, +) => { + const userClient = await getUserClientWithDetails(userId, clientId); + if (!userClient) { + error(500, "Invalid access token"); + } + + const data = JSON.stringify({ version, key: encMek }); + return verifySignature(Buffer.from(data), encMekSig, userClient.client.sigPubKey); +}; diff --git a/src/lib/server/services/mek.ts b/src/lib/server/services/mek.ts index a3c8449..9fc0452 100644 --- a/src/lib/server/services/mek.ts +++ b/src/lib/server/services/mek.ts @@ -1,14 +1,7 @@ import { error } from "@sveltejs/kit"; -import { getAllUserClients, setUserClientStateToActive } from "$lib/server/db/client"; -import { - registerInitialMek, - registerActiveMek, - getAllValidMeks, - getNextActiveMekVersion, - registerClientMeks, - getAllValidClientMeks, -} from "$lib/server/db/mek"; -import { isInitialMekNeeded } from "$lib/server/modules/mek"; +import { setUserClientStateToActive } from "$lib/server/db/client"; +import { registerInitialMek, getAllValidClientMeks } from "$lib/server/db/mek"; +import { isInitialMekNeeded, verifyClientEncMekSig } from "$lib/server/modules/mek"; export const getClientMekList = async (userId: number, clientId: number) => { const clientMeks = await getAllValidClientMeks(userId, clientId); @@ -17,6 +10,7 @@ export const getClientMekList = async (userId: number, clientId: number) => { version: clientMek.master_encryption_key.version, state: clientMek.master_encryption_key.state, mek: clientMek.client_master_encryption_key.encMek, + mekSig: clientMek.client_master_encryption_key.encMekSig, })), }; }; @@ -25,53 +19,14 @@ export const registerInitialActiveMek = async ( userId: number, createdBy: number, encMek: string, + encMekSig: string, ) => { if (!(await isInitialMekNeeded(userId))) { error(409, "Initial MEK already registered"); + } else if (!(await verifyClientEncMekSig(userId, createdBy, 1, encMek, encMekSig))) { + error(400, "Invalid signature"); } - await registerInitialMek(userId, createdBy, encMek); + await registerInitialMek(userId, createdBy, encMek, encMekSig); await setUserClientStateToActive(userId, createdBy); }; - -export const registerNewActiveMek = async ( - userId: number, - createdBy: number, - clientMeks: { - clientId: number; - encMek: string; - }[], -) => { - const userClients = await getAllUserClients(userId); - const activeUserClients = userClients.filter(({ state }) => state === "active"); - if ( - clientMeks.length !== activeUserClients.length || - !clientMeks.every((clientMek) => - activeUserClients.some((userClient) => userClient.clientId === clientMek.clientId), - ) - ) { - error(400, "Invalid key list"); - } - - const newMekVersion = await getNextActiveMekVersion(userId); - await registerActiveMek(userId, newMekVersion, createdBy, clientMeks); -}; - -export const shareMeksForNewClient = async ( - userId: number, - targetClientId: number, - clientMeks: { - version: number; - encMek: string; - }[], -) => { - const meks = await getAllValidMeks(userId); - if ( - clientMeks.length !== meks.length || - !clientMeks.every((clientMek) => meks.some((mek) => mek.version === clientMek.version)) - ) { - error(400, "Invalid key list"); - } - - await registerClientMeks(userId, targetClientId, clientMeks); -}; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index d55cb93..2eba920 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -7,6 +7,7 @@ import { signRSAMessage, makeAESKeyNonextractable, unwrapAESKeyUsingRSA, + verifyMasterKeyWrappedSig, } from "$lib/modules/crypto"; import { masterKeyStore } from "$lib/stores"; @@ -45,7 +46,7 @@ export const requestClientRegistration = async ( return res.ok; }; -export const requestMasterKeyDownload = async (decryptKey: CryptoKey) => { +export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: CryptoKey) => { const res = await callAPI("/api/mek/list", { method: "GET" }); if (!res.ok) return false; @@ -55,17 +56,28 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey) => { version: number; state: "active" | "retired"; mek: string; + mekSig: string; }[]; }; + const masterKeys = await Promise.all( - masterKeysWrapped.map(async ({ version, state, mek: masterKeyWrapped }) => ({ - version, - state, - masterKey: await makeAESKeyNonextractable( - await unwrapAESKeyUsingRSA(decodeFromBase64(masterKeyWrapped), decryptKey), - ), - })), + masterKeysWrapped.map( + async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => ({ + version, + state, + masterKey: await makeAESKeyNonextractable( + await unwrapAESKeyUsingRSA(decodeFromBase64(masterKeyWrapped), decryptKey), + ), + isValid: await verifyMasterKeyWrappedSig( + version, + masterKeyWrapped, + masterKeyWrappedSig, + verfiyKey, + ), + }), + ), ); + if (!masterKeys.every(({ isValid }) => isValid)) return false; await storeMasterKeys( masterKeys.map(({ version, state, masterKey }) => ({ version, state, key: masterKey })), diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index 776bb6d..299a523 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -29,7 +29,10 @@ // TODO: Multi-user support - if ($masterKeyStore || (await requestMasterKeyDownload($clientKeyStore.decryptKey))) { + if ( + $masterKeyStore || + (await requestMasterKeyDownload($clientKeyStore.decryptKey, $clientKeyStore.verifyKey)) + ) { await goto(data.redirectPath); } else { await redirect("/client/pending"); diff --git a/src/routes/(fullscreen)/client/pending/+page.svelte b/src/routes/(fullscreen)/client/pending/+page.svelte index f362bd5..3d000b2 100644 --- a/src/routes/(fullscreen)/client/pending/+page.svelte +++ b/src/routes/(fullscreen)/client/pending/+page.svelte @@ -16,11 +16,13 @@ if ($masterKeyStore) { goto(data.redirectPath); } else if ($clientKeyStore) { - requestMasterKeyDownload($clientKeyStore.decryptKey).then(async (ok) => { - if (ok) { - return await goto(data.redirectPath); - } - }); + requestMasterKeyDownload($clientKeyStore.decryptKey, $clientKeyStore.verifyKey).then( + async (ok) => { + if (ok) { + return await goto(data.redirectPath); + } + }, + ); } }); diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index 68c4f60..cc8e7be 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,6 +1,6 @@ import { callAPI } from "$lib/hooks"; import { storeClientKey } from "$lib/indexedDB"; -import { encodeToBase64, signRequest } from "$lib/modules/crypto"; +import { encodeToBase64, signRequest, signMasterKeyWrapped } from "$lib/modules/crypto"; import type { ClientKeys } from "$lib/stores"; export { requestTokenUpgrade } from "$lib/services/auth"; @@ -53,6 +53,7 @@ export const requestInitialMasterKeyRegistration = async ( body: await signRequest( { mek: encodeToBase64(masterKeyWrapped), + mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey), }, signKey, ), diff --git a/src/routes/api/mek/register/+server.ts b/src/routes/api/mek/register/+server.ts deleted file mode 100644 index ab454a7..0000000 --- a/src/routes/api/mek/register/+server.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { text } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; -import { registerNewActiveMek } from "$lib/server/services/mek"; -import type { RequestHandler } from "@sveltejs/kit"; - -export const POST: RequestHandler = async ({ request, cookies }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); - const { meks } = await parseSignedRequest( - clientId, - await request.json(), - z.object({ - meks: z.array( - z.object({ - clientId: z.number().int().positive(), - mek: z.string().base64().nonempty(), - }), - ), - }), - ); - - await registerNewActiveMek( - userId, - clientId, - meks.map(({ clientId, mek }) => ({ - clientId, - encMek: mek, - })), - ); - return text("MEK registered", { headers: { "Content-Type": "text/plain" } }); -}; diff --git a/src/routes/api/mek/register/initial/+server.ts b/src/routes/api/mek/register/initial/+server.ts index 0f495ff..a5aa6d9 100644 --- a/src/routes/api/mek/register/initial/+server.ts +++ b/src/routes/api/mek/register/initial/+server.ts @@ -11,14 +11,15 @@ export const POST: RequestHandler = async ({ request, cookies }) => { error(403, "Forbidden"); } - const { mek } = await parseSignedRequest( + const { mek, mekSig } = await parseSignedRequest( clientId, await request.json(), z.object({ mek: z.string().base64().nonempty(), + mekSig: z.string().base64().nonempty(), }), ); - await registerInitialActiveMek(userId, clientId, mek); + await registerInitialActiveMek(userId, clientId, mek, mekSig); return text("MEK registered", { headers: { "Content-Type": "text/plain" } }); }; diff --git a/src/routes/api/mek/share/+server.ts b/src/routes/api/mek/share/+server.ts deleted file mode 100644 index 122e659..0000000 --- a/src/routes/api/mek/share/+server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { text } from "@sveltejs/kit"; -import { z } from "zod"; -import { authorize } from "$lib/server/modules/auth"; -import { parseSignedRequest } from "$lib/server/modules/crypto"; -import { shareMeksForNewClient } from "$lib/server/services/mek"; -import type { RequestHandler } from "./$types"; - -export const POST: RequestHandler = async ({ request, cookies }) => { - const { userId, clientId } = await authorize(cookies, "activeClient"); - const { targetId, meks } = await parseSignedRequest( - clientId, - await request.json(), - z.object({ - targetId: z.number().int().positive(), - meks: z.array( - z.object({ - version: z.number().int().positive(), - mek: z.string().base64().nonempty(), - }), - ), - }), - ); - - await shareMeksForNewClient( - userId, - targetId, - meks.map(({ version, mek }) => ({ version, encMek: mek })), - ); - return text("MEK shared", { headers: { "Content-Type": "text/plain" } }); -};