diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/hooks/gotoStateful.ts index 064be29..a6f7bd5 100644 --- a/src/lib/hooks/gotoStateful.ts +++ b/src/lib/hooks/gotoStateful.ts @@ -6,6 +6,7 @@ interface KeyExportState { redirectPath: string; pubKeyBase64: string; privKeyBase64: string; + mekDraft: ArrayBuffer; } const useAutoNull = (value: T | null) => { diff --git a/src/lib/modules/crypto.ts b/src/lib/modules/crypto.ts index 93a7e22..d3fe0d2 100644 --- a/src/lib/modules/crypto.ts +++ b/src/lib/modules/crypto.ts @@ -1,5 +1,13 @@ export type RSAKeyType = "public" | "private"; +export const encodeToBase64 = (data: ArrayBuffer) => { + return btoa(String.fromCharCode(...new Uint8Array(data))); +}; + +export const decodeFromBase64 = (data: string) => { + return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer; +}; + export const generateRSAKeyPair = async () => { const keyPair = await window.crypto.subtle.generateKey( { @@ -15,36 +23,71 @@ export const generateRSAKeyPair = async () => { }; export const makeRSAKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) => { - const format = type === "public" ? "spki" : "pkcs8"; - const keyUsage = type === "public" ? "encrypt" : "decrypt"; + const { format, key: exportedKey } = await exportRSAKey(key, type); return await window.crypto.subtle.importKey( format, - await window.crypto.subtle.exportKey(format, key), + exportedKey, { name: "RSA-OAEP", hash: "SHA-256", } satisfies RsaHashedImportParams, false, - [keyUsage], + [type === "public" ? "encrypt" : "decrypt"], ); }; -export const exportRSAKeyToBase64 = async (key: CryptoKey, type: RSAKeyType) => { - const exportedKey = await window.crypto.subtle.exportKey( - type === "public" ? "spki" : "pkcs8", - key, - ); - return btoa(String.fromCharCode(...new Uint8Array(exportedKey))); +export const exportRSAKey = async (key: CryptoKey, type: RSAKeyType) => { + const format = type === "public" ? ("spki" as const) : ("pkcs8" as const); + return { + format, + key: await window.crypto.subtle.exportKey(format, key), + }; }; -export const decryptRSACiphertext = async (ciphertext: string, privateKey: CryptoKey) => { - const ciphertextBuffer = Uint8Array.from(atob(ciphertext), (c) => c.charCodeAt(0)); - const plaintext = await window.crypto.subtle.decrypt( +export const encryptRSAPlaintext = async (plaintext: ArrayBuffer, publicKey: CryptoKey) => { + return await window.crypto.subtle.encrypt( + { + name: "RSA-OAEP", + } satisfies RsaOaepParams, + publicKey, + plaintext, + ); +}; + +export const decryptRSACiphertext = async (ciphertext: ArrayBuffer, privateKey: CryptoKey) => { + return await window.crypto.subtle.decrypt( { name: "RSA-OAEP", } satisfies RsaOaepParams, privateKey, - ciphertextBuffer, + ciphertext, ); - return btoa(String.fromCharCode(...new Uint8Array(plaintext))); +}; + +export const generateAESKey = async () => { + return await window.crypto.subtle.generateKey( + { + name: "AES-GCM", + length: 256, + } satisfies AesKeyGenParams, + true, + ["encrypt", "decrypt"], + ); +}; + +export const makeAESKeyNonextractable = async (key: CryptoKey) => { + return await window.crypto.subtle.importKey( + "raw", + await exportAESKey(key), + { + name: "AES-GCM", + length: 256, + } satisfies AesKeyAlgorithm, + false, + ["encrypt", "decrypt"], + ); +}; + +export const exportAESKey = async (key: CryptoKey) => { + return await window.crypto.subtle.exportKey("raw", key); }; diff --git a/src/lib/server/db/client.ts b/src/lib/server/db/client.ts index 53218c9..c31579b 100644 --- a/src/lib/server/db/client.ts +++ b/src/lib/server/db/client.ts @@ -1,4 +1,4 @@ -import { and, eq, gt, lte, count } from "drizzle-orm"; +import { and, eq, gt, lte } from "drizzle-orm"; import db from "./drizzle"; import { client, userClient, userClientChallenge } from "./schema"; diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts index f8dc6ac..4b806c7 100644 --- a/src/lib/stores/key.ts +++ b/src/lib/stores/key.ts @@ -1,3 +1,4 @@ import { writable } from "svelte/store"; export const keyPairStore = writable(null); +export const mekStore = writable>(new Map()); diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 7b5863c..d8abe33 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,4 +1,4 @@ -import { exportRSAKeyToBase64 } from "$lib/modules/crypto"; +import { encodeToBase64, exportRSAKey } from "$lib/modules/crypto"; import { requestPubKeyRegistration } from "../../key/export/service"; const callLoginAPI = async (email: string, password: string, pubKeyBase64?: string) => { @@ -22,7 +22,7 @@ export const requestLogin = async ( registerPubKey = true, ): Promise => { const pubKeyBase64 = keyPair - ? await exportRSAKeyToBase64(keyPair.publicKey, "public") + ? encodeToBase64((await exportRSAKey(keyPair.publicKey, "public")).key) : undefined; let loginRes = await callLoginAPI(email, password, pubKeyBase64); if (loginRes.ok) { diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index 762fdc9..267fd18 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -9,8 +9,9 @@ import { createBlobFromKeyPairBase64, requestPubKeyRegistration, - requestTokenUpgrade, storeKeyPairPersistently, + requestTokenUpgrade, + requestInitialMekRegistration, } from "./service"; import IconKey from "~icons/material-symbols/key"; @@ -39,16 +40,22 @@ isBeforeContinueModalOpen = false; isBeforeContinueBottomSheetOpen = false; - if (await requestPubKeyRegistration(data.pubKeyBase64, $keyPairStore.privateKey)) { + try { + if (!(await requestPubKeyRegistration(data.pubKeyBase64, $keyPairStore.privateKey))) + throw new Error("Failed to register public key"); + await storeKeyPairPersistently($keyPairStore); - if (await requestTokenUpgrade(data.pubKeyBase64)) { - await goto(data.redirectPath); - } else { - // TODO: Error handling - } - } else { + if (!(await requestTokenUpgrade(data.pubKeyBase64))) + throw new Error("Failed to upgrade token"); + + if (!(await requestInitialMekRegistration(data.mekDraft, $keyPairStore.publicKey))) + throw new Error("Failed to register initial MEK"); + + await goto(data.redirectPath); + } catch (e) { // TODO: Error handling + throw e; } }; diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index 09031c4..cd0dd89 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,6 +1,11 @@ import { callAPI } from "$lib/hooks"; import { storeKeyPairIntoIndexedDB } from "$lib/indexedDB"; -import { decryptRSACiphertext } from "$lib/modules/crypto"; +import { + encodeToBase64, + decodeFromBase64, + encryptRSAPlaintext, + decryptRSACiphertext, +} from "$lib/modules/crypto"; export const createBlobFromKeyPairBase64 = (pubKeyBase64: string, privKeyBase64: string) => { const pubKeyFormatted = pubKeyBase64.match(/.{1,64}/g)?.join("\n"); @@ -26,18 +31,22 @@ export const requestPubKeyRegistration = async (pubKeyBase64: string, privateKey const data = await res.json(); const challenge = data.challenge as string; - const answer = await decryptRSACiphertext(challenge, privateKey); + const answer = await decryptRSACiphertext(decodeFromBase64(challenge), privateKey); res = await callAPI("/api/client/verify", { method: "POST", headers: { "Content-Type": "application/json", }, - body: JSON.stringify({ answer }), + body: JSON.stringify({ answer: encodeToBase64(answer) }), }); return res.ok; }; +export const storeKeyPairPersistently = async (keyPair: CryptoKeyPair) => { + await storeKeyPairIntoIndexedDB(keyPair.publicKey, keyPair.privateKey); +}; + export const requestTokenUpgrade = async (pubKeyBase64: string) => { const res = await fetch("/api/auth/upgradeToken", { method: "POST", @@ -49,6 +58,17 @@ export const requestTokenUpgrade = async (pubKeyBase64: string) => { return res.ok; }; -export const storeKeyPairPersistently = async (keyPair: CryptoKeyPair) => { - await storeKeyPairIntoIndexedDB(keyPair.publicKey, keyPair.privateKey); +export const requestInitialMekRegistration = async ( + mekDraft: ArrayBuffer, + publicKey: CryptoKey, +) => { + const mekDraftEncrypted = await encryptRSAPlaintext(mekDraft, publicKey); + const res = await callAPI("/api/mek/register/initial", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ mek: encodeToBase64(mekDraftEncrypted) }), + }); + return res.ok || res.status === 403; }; diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 3a751b6..01bf7d3 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -5,7 +5,7 @@ import { gotoStateful } from "$lib/hooks"; import { keyPairStore } from "$lib/stores"; import Order from "./Order.svelte"; - import { generateKeyPair } from "./service"; + import { generateKeyPair, generateMekDraft } from "./service"; import IconKey from "~icons/material-symbols/key"; @@ -33,11 +33,14 @@ const generate = async () => { // TODO: Loading indicator - const keyPair = await generateKeyPair(); + const { pubKeyBase64, privKeyBase64 } = await generateKeyPair(); + const { mekDraft } = await generateMekDraft(); + await gotoStateful("/key/export", { redirectPath: data.redirectPath, - pubKeyBase64: keyPair.pubKeyBase64, - privKeyBase64: keyPair.privKeyBase64, + pubKeyBase64, + privKeyBase64, + mekDraft, }); }; diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index 360263e..8900fd3 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -1,9 +1,13 @@ import { + encodeToBase64, generateRSAKeyPair, makeRSAKeyNonextractable, - exportRSAKeyToBase64, + exportRSAKey, + generateAESKey, + makeAESKeyNonextractable, + exportAESKey, } from "$lib/modules/crypto"; -import { keyPairStore } from "$lib/stores"; +import { keyPairStore, mekStore } from "$lib/stores"; export const generateKeyPair = async () => { const keyPair = await generateRSAKeyPair(); @@ -15,7 +19,21 @@ export const generateKeyPair = async () => { }); return { - pubKeyBase64: await exportRSAKeyToBase64(keyPair.publicKey, "public"), - privKeyBase64: await exportRSAKeyToBase64(keyPair.privateKey, "private"), + pubKeyBase64: encodeToBase64((await exportRSAKey(keyPair.publicKey, "public")).key), + privKeyBase64: encodeToBase64((await exportRSAKey(keyPair.privateKey, "private")).key), + }; +}; + +export const generateMekDraft = async () => { + const mek = await generateAESKey(); + const mekSecured = await makeAESKeyNonextractable(mek); + + mekStore.update((meks) => { + meks.set(meks.size, mekSecured); + return meks; + }); + + return { + mekDraft: await exportAESKey(mek), }; }; diff --git a/src/routes/api/mek/list/+server.ts b/src/routes/api/mek/list/+server.ts index ed0f9bf..4801df1 100644 --- a/src/routes/api/mek/list/+server.ts +++ b/src/routes/api/mek/list/+server.ts @@ -1,4 +1,4 @@ -import { error, json } from "@sveltejs/kit"; +import { json } from "@sveltejs/kit"; import { authorize } from "$lib/server/modules/auth"; import { getClientMekList } from "$lib/server/services/mek"; import type { RequestHandler } from "@sveltejs/kit";