From 0ef252913ae0fdf5d62fa0e16c9ca6c683990d04 Mon Sep 17 00:00:00 2001 From: static Date: Tue, 31 Dec 2024 04:18:34 +0900 Subject: [PATCH] =?UTF-8?q?=EC=95=94=ED=98=B8=20=ED=82=A4=20=EC=83=9D?= =?UTF-8?q?=EC=84=B1=20=ED=8E=98=EC=9D=B4=EC=A7=80=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=82=A4=EC=99=80=20=EC=84=9C=EB=AA=85?= =?UTF-8?q?=ED=82=A4=EB=A5=BC=20=ED=95=A8=EA=BB=98=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=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; + } +};