diff --git a/src/lib/modules/crypto/rsa.ts b/src/lib/modules/crypto/rsa.ts index 4df8f9e..13dfd46 100644 --- a/src/lib/modules/crypto/rsa.ts +++ b/src/lib/modules/crypto/rsa.ts @@ -46,6 +46,56 @@ export const exportRSAKeyToBase64 = async (key: CryptoKey) => { return encodeToBase64((await exportRSAKey(key)).key); }; +export const importEncryptionKeyPairFromBase64 = async ( + encryptKeyBase64: string, + decryptKeyBase64: string, +) => { + const algorithm: RsaHashedImportParams = { + name: "RSA-OAEP", + hash: "SHA-256", + }; + const encryptKey = await window.crypto.subtle.importKey( + "spki", + decodeFromBase64(encryptKeyBase64), + algorithm, + true, + ["encrypt", "wrapKey"], + ); + const decryptKey = await window.crypto.subtle.importKey( + "pkcs8", + decodeFromBase64(decryptKeyBase64), + algorithm, + true, + ["decrypt", "unwrapKey"], + ); + return { encryptKey, decryptKey }; +}; + +export const importSigningKeyPairFromBase64 = async ( + signKeyBase64: string, + verifyKeyBase64: string, +) => { + const algorithm: RsaHashedImportParams = { + name: "RSA-PSS", + hash: "SHA-256", + }; + const signKey = await window.crypto.subtle.importKey( + "pkcs8", + decodeFromBase64(signKeyBase64), + algorithm, + true, + ["sign"], + ); + const verifyKey = await window.crypto.subtle.importKey( + "spki", + decodeFromBase64(verifyKeyBase64), + algorithm, + true, + ["verify"], + ); + return { signKey, verifyKey }; +}; + export const makeRSAKeyNonextractable = async (key: CryptoKey) => { const { key: exportedKey, format } = await exportRSAKey(key); return await window.crypto.subtle.importKey( diff --git a/src/lib/modules/key.ts b/src/lib/modules/key.ts new file mode 100644 index 0000000..945902c --- /dev/null +++ b/src/lib/modules/key.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; +import { storeClientKey } from "$lib/indexedDB"; +import type { ClientKeys } from "$lib/stores"; + +const serializedClientKeysSchema = z.intersection( + z.object({ + generator: z.literal("ArkVault"), + exportedAt: z.string().datetime(), + }), + z.object({ + version: z.literal(1), + encryptKey: z.string().base64().nonempty(), + decryptKey: z.string().base64().nonempty(), + signKey: z.string().base64().nonempty(), + verifyKey: z.string().base64().nonempty(), + }), +); + +type SerializedClientKeys = z.infer; + +type DeserializedClientKeys = { + encryptKeyBase64: string; + decryptKeyBase64: string; + signKeyBase64: string; + verifyKeyBase64: string; +}; + +export const serializeClientKeys = ({ + encryptKeyBase64, + decryptKeyBase64, + signKeyBase64, + verifyKeyBase64, +}: DeserializedClientKeys) => { + return JSON.stringify({ + version: 1, + generator: "ArkVault", + exportedAt: new Date().toISOString(), + encryptKey: encryptKeyBase64, + decryptKey: decryptKeyBase64, + signKey: signKeyBase64, + verifyKey: verifyKeyBase64, + } satisfies SerializedClientKeys); +}; + +export const deserializeClientKeys = (serialized: string) => { + const zodRes = serializedClientKeysSchema.safeParse(JSON.parse(serialized)); + if (zodRes.success) { + return { + encryptKeyBase64: zodRes.data.encryptKey, + decryptKeyBase64: zodRes.data.decryptKey, + signKeyBase64: zodRes.data.signKey, + verifyKeyBase64: zodRes.data.verifyKey, + } satisfies DeserializedClientKeys; + } + return undefined; +}; + +export const storeClientKeys = async (clientKeys: ClientKeys) => { + await Promise.all([ + storeClientKey(clientKeys.encryptKey, "encrypt"), + storeClientKey(clientKeys.decryptKey, "decrypt"), + storeClientKey(clientKeys.signKey, "sign"), + storeClientKey(clientKeys.verifyKey, "verify"), + ]); +}; diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index a7e1c08..cecd241 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -2,18 +2,23 @@ import { callGetApi, callPostApi } from "$lib/hooks"; import { storeMasterKeys } from "$lib/indexedDB"; import { encodeToBase64, + exportRSAKeyToBase64, decryptChallenge, signMessageRSA, unwrapMasterKey, + signMasterKeyWrapped, verifyMasterKeyWrapped, } from "$lib/modules/crypto"; import type { ClientRegisterRequest, ClientRegisterResponse, ClientRegisterVerifyRequest, + InitialHmacSecretRegisterRequest, MasterKeyListResponse, + InitialMasterKeyRegisterRequest, } from "$lib/server/schemas"; -import { masterKeyStore } from "$lib/stores"; +import { requestSessionUpgrade } from "$lib/services/auth"; +import { masterKeyStore, type ClientKeys } from "$lib/stores"; export const requestClientRegistration = async ( encryptKeyBase64: string, @@ -38,6 +43,35 @@ export const requestClientRegistration = async ( return res.ok; }; +export const requestClientRegistrationAndSessionUpgrade = async ( + { encryptKey, decryptKey, signKey, verifyKey }: ClientKeys, + force: boolean, +) => { + const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey); + const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey); + const [res, error] = await requestSessionUpgrade( + encryptKeyBase64, + decryptKey, + verifyKeyBase64, + signKey, + force, + ); + if (error === undefined) return [res] as const; + + if ( + error === "Unregistered client" && + !(await requestClientRegistration(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) + ) { + return [false] as const; + } else if (error === "Already logged in") { + return [false, force ? undefined : error] as const; + } + + return [ + (await requestSessionUpgrade(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey))[0], + ] as const; +}; + export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verifyKey: CryptoKey) => { const res = await callGetApi("/api/mek/list"); if (!res.ok) return false; @@ -68,3 +102,23 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verifyKey: return true; }; + +export const requestInitialMasterKeyAndHmacSecretRegistration = async ( + masterKeyWrapped: string, + hmacSecretWrapped: string, + signKey: CryptoKey, +) => { + let res = await callPostApi("/api/mek/register/initial", { + mek: masterKeyWrapped, + mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey), + }); + if (!res.ok) { + return res.status === 403 || res.status === 409; + } + + res = await callPostApi("/api/hsk/register/initial", { + mekVersion: 1, + hsk: hmacSecretWrapped, + }); + return res.ok; +}; diff --git a/src/routes/(fullscreen)/auth/login/+page.svelte b/src/routes/(fullscreen)/auth/login/+page.svelte index 00ee71e..0adfe98 100644 --- a/src/routes/(fullscreen)/auth/login/+page.svelte +++ b/src/routes/(fullscreen)/auth/login/+page.svelte @@ -7,7 +7,7 @@ import { requestLogout, requestLogin, - requestSessionUpgrade, + requestClientRegistrationAndSessionUpgrade, requestMasterKeyDownload, } from "./service"; @@ -24,7 +24,10 @@ const upgradeSession = async (force: boolean) => { try { - const [upgradeRes, upgradeError] = await requestSessionUpgrade($clientKeyStore!, force); + const [upgradeRes, upgradeError] = await requestClientRegistrationAndSessionUpgrade( + $clientKeyStore!, + force, + ); if (!force && upgradeError === "Already logged in") { isForceLoginModalOpen = true; return; diff --git a/src/routes/(fullscreen)/auth/login/service.ts b/src/routes/(fullscreen)/auth/login/service.ts index 43e7c0d..ada0f5f 100644 --- a/src/routes/(fullscreen)/auth/login/service.ts +++ b/src/routes/(fullscreen)/auth/login/service.ts @@ -1,45 +1,13 @@ import { callPostApi } from "$lib/hooks"; -import { exportRSAKeyToBase64 } from "$lib/modules/crypto"; import type { LoginRequest } from "$lib/server/schemas"; -import { requestSessionUpgrade as requestSessionUpgradeInternal } from "$lib/services/auth"; -import { requestClientRegistration } from "$lib/services/key"; -import type { ClientKeys } from "$lib/stores"; export { requestLogout } from "$lib/services/auth"; -export { requestMasterKeyDownload } from "$lib/services/key"; +export { + requestClientRegistrationAndSessionUpgrade, + requestMasterKeyDownload, +} from "$lib/services/key"; export const requestLogin = async (email: string, password: string) => { const res = await callPostApi("/api/auth/login", { email, password }); return res.ok; }; - -export const requestSessionUpgrade = async ( - { encryptKey, decryptKey, signKey, verifyKey }: ClientKeys, - force: boolean, -) => { - const encryptKeyBase64 = await exportRSAKeyToBase64(encryptKey); - const verifyKeyBase64 = await exportRSAKeyToBase64(verifyKey); - const [res, error] = await requestSessionUpgradeInternal( - encryptKeyBase64, - decryptKey, - verifyKeyBase64, - signKey, - force, - ); - if (error === undefined) return [res] as const; - - if ( - error === "Unregistered client" && - !(await requestClientRegistration(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey)) - ) { - return [false] as const; - } else if (error === "Already logged in") { - return [false, force ? undefined : error] as const; - } - - return [ - ( - await requestSessionUpgradeInternal(encryptKeyBase64, decryptKey, verifyKeyBase64, signKey) - )[0], - ] as const; -}; diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index ac83504..80e8eee 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -3,13 +3,12 @@ import { goto } from "$app/navigation"; import { BottomDiv, Button, FullscreenDiv, TextButton } from "$lib/components/atoms"; import { TitledDiv } from "$lib/components/molecules"; + import { serializeClientKeys, storeClientKeys } from "$lib/modules/key"; import { clientKeyStore } from "$lib/stores"; import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte"; import BeforeContinueModal from "./BeforeContinueModal.svelte"; import { - serializeClientKeys, requestClientRegistration, - storeClientKeys, requestSessionUpgrade, requestInitialMasterKeyAndHmacSecretRegistration, } from "./service"; @@ -22,15 +21,8 @@ let isBeforeContinueBottomSheetOpen = $state(false); const exportClientKeys = () => { - const clientKeysSerialized = serializeClientKeys( - data.encryptKeyBase64, - data.decryptKeyBase64, - data.signKeyBase64, - data.verifyKeyBase64, - ); - const clientKeysBlob = new Blob([JSON.stringify(clientKeysSerialized)], { - type: "application/json", - }); + const clientKeysSerialized = serializeClientKeys(data); + const clientKeysBlob = new Blob([clientKeysSerialized], { type: "application/json" }); FileSaver.saveAs(clientKeysBlob, "arkvault-clientkey.json"); if (!isBeforeContinueBottomSheetOpen) { diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index a9aaaee..ddc3dee 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,68 +1,5 @@ -import { callPostApi } from "$lib/hooks"; -import { storeClientKey } from "$lib/indexedDB"; -import { signMasterKeyWrapped } from "$lib/modules/crypto"; -import type { - InitialMasterKeyRegisterRequest, - InitialHmacSecretRegisterRequest, -} from "$lib/server/schemas"; -import type { ClientKeys } from "$lib/stores"; - export { requestSessionUpgrade } from "$lib/services/auth"; -export { requestClientRegistration } from "$lib/services/key"; - -type SerializedKeyPairs = { - generator: "ArkVault"; - exportedAt: Date; -} & { - version: 1; - encryptKey: string; - decryptKey: string; - signKey: string; - verifyKey: string; -}; - -export const serializeClientKeys = ( - encryptKeyBase64: string, - decryptKeyBase64: string, - signKeyBase64: string, - verifyKeyBase64: string, -) => { - return { - version: 1, - generator: "ArkVault", - exportedAt: new Date(), - encryptKey: encryptKeyBase64, - decryptKey: decryptKeyBase64, - signKey: signKeyBase64, - verifyKey: verifyKeyBase64, - } satisfies SerializedKeyPairs; -}; - -export const storeClientKeys = async (clientKeys: ClientKeys) => { - await Promise.all([ - storeClientKey(clientKeys.encryptKey, "encrypt"), - storeClientKey(clientKeys.decryptKey, "decrypt"), - storeClientKey(clientKeys.signKey, "sign"), - storeClientKey(clientKeys.verifyKey, "verify"), - ]); -}; - -export const requestInitialMasterKeyAndHmacSecretRegistration = async ( - masterKeyWrapped: string, - hmacSecretWrapped: string, - signKey: CryptoKey, -) => { - let res = await callPostApi("/api/mek/register/initial", { - mek: masterKeyWrapped, - mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey), - }); - if (!res.ok) { - return res.status === 409; - } - - res = await callPostApi("/api/hsk/register/initial", { - mekVersion: 1, - hsk: hmacSecretWrapped, - }); - return res.ok; -}; +export { + requestClientRegistration, + requestInitialMasterKeyAndHmacSecretRegistration, +} from "$lib/services/key"; diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 03d6c6d..141c085 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -4,18 +4,27 @@ import { BottomDiv, Button, FullscreenDiv, TextButton } from "$lib/components/atoms"; import { TitledDiv } from "$lib/components/molecules"; import { gotoStateful } from "$lib/hooks"; + import { storeClientKeys } from "$lib/modules/key"; import { clientKeyStore } from "$lib/stores"; + import ForceLoginModal from "./ForceLoginModal.svelte"; import Order from "./Order.svelte"; import { generateClientKeys, generateInitialMasterKey, generateInitialHmacSecret, + importClientKeys, + requestClientRegistrationAndSessionUpgrade, + requestInitialMasterKeyAndHmacSecretRegistration, } from "./service"; import IconKey from "~icons/material-symbols/key"; let { data } = $props(); + let fileInput: HTMLInputElement | undefined = $state(); + + let isForceLoginModalOpen = $state(false); + // TODO: Update const orders = [ { @@ -51,6 +60,53 @@ }); }; + const importKeys = async () => { + const file = fileInput?.files?.[0]; + if (!file) return; + + if (await importClientKeys(await file.text())) { + await upgradeSession(false); + } else { + // TODO: Error Handling + } + + fileInput!.value = ""; + }; + + const upgradeSession = async (force: boolean) => { + const [upgradeRes, upgradeError] = await requestClientRegistrationAndSessionUpgrade( + $clientKeyStore!, + force, + ); + if (!force && upgradeError === "Already logged in") { + isForceLoginModalOpen = true; + return; + } else if (!upgradeRes) { + // TODO: Error Handling + return; + } + + const { masterKey, masterKeyWrapped } = await generateInitialMasterKey( + $clientKeyStore!.encryptKey, + ); + const { hmacSecretWrapped } = await generateInitialHmacSecret(masterKey); + + await storeClientKeys($clientKeyStore!); + + if ( + !(await requestInitialMasterKeyAndHmacSecretRegistration( + masterKeyWrapped, + hmacSecretWrapped, + $clientKeyStore!.signKey, + )) + ) { + // TODO: Error Handling + return; + } + + await goto("/client/pending?redirect=" + encodeURIComponent(data.redirectPath)); + }; + onMount(async () => { if ($clientKeyStore) { await goto(data.redirectPath, { replaceState: true }); @@ -62,6 +118,14 @@ 암호 키 생성하기 + + {#snippet title()} @@ -83,6 +147,8 @@ - 키를 갖고 있어요 + fileInput?.click()}>키를 갖고 있어요 + + upgradeSession(true)} /> diff --git a/src/routes/(fullscreen)/key/generate/ForceLoginModal.svelte b/src/routes/(fullscreen)/key/generate/ForceLoginModal.svelte new file mode 100644 index 0000000..f488603 --- /dev/null +++ b/src/routes/(fullscreen)/key/generate/ForceLoginModal.svelte @@ -0,0 +1,20 @@ + + + +

다른 디바이스에서는 로그아웃하고, 이 디바이스에서 로그인할까요?

+
diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index f970f46..2faef15 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -2,6 +2,8 @@ import { generateEncryptionKeyPair, generateSigningKeyPair, exportRSAKeyToBase64, + importEncryptionKeyPairFromBase64, + importSigningKeyPairFromBase64, makeRSAKeyNonextractable, wrapMasterKey, generateMasterKey, @@ -9,8 +11,15 @@ import { wrapHmacSecret, generateHmacSecret, } from "$lib/modules/crypto"; +import { deserializeClientKeys } from "$lib/modules/key"; import { clientKeyStore } from "$lib/stores"; +export { requestLogout } from "$lib/services/auth"; +export { + requestClientRegistrationAndSessionUpgrade, + requestInitialMasterKeyAndHmacSecretRegistration, +} from "$lib/services/key"; + export const generateClientKeys = async () => { const { encryptKey, decryptKey } = await generateEncryptionKeyPair(); const { signKey, verifyKey } = await generateSigningKeyPair(); @@ -45,3 +54,25 @@ export const generateInitialHmacSecret = async (masterKey: CryptoKey) => { hmacSecretWrapped: await wrapHmacSecret(hmacSecret, masterKey), }; }; + +export const importClientKeys = async (clientKeysSerialized: string) => { + const clientKeys = deserializeClientKeys(clientKeysSerialized); + if (!clientKeys) return false; + + const { encryptKey, decryptKey } = await importEncryptionKeyPairFromBase64( + clientKeys.encryptKeyBase64, + clientKeys.decryptKeyBase64, + ); + const { signKey, verifyKey } = await importSigningKeyPairFromBase64( + clientKeys.signKeyBase64, + clientKeys.verifyKeyBase64, + ); + + clientKeyStore.set({ + encryptKey, + decryptKey: await makeRSAKeyNonextractable(decryptKey), + signKey: await makeRSAKeyNonextractable(signKey), + verifyKey, + }); + return true; +};