클라이언트 승인 대기 페이지 구현

This commit is contained in:
static
2024-12-31 21:58:13 +09:00
parent ccad4fbd8b
commit e5cbd46b35
13 changed files with 243 additions and 59 deletions

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { TitleDiv } from "$lib/components/divs";
import { clientKeyStore, masterKeyStore } from "$lib/stores";
import { generateEncryptKeyFingerprint, requestMasterKeyDownload } from "./service";
import IconFingerprint from "~icons/material-symbols/fingerprint";
let { data } = $props();
let fingerprint = $derived(
$clientKeyStore ? generateEncryptKeyFingerprint($clientKeyStore.encryptKey) : undefined,
);
$effect(() => {
if ($masterKeyStore) {
goto(data.redirectPath);
} else if ($clientKeyStore) {
requestMasterKeyDownload($clientKeyStore.decryptKey).then(async (ok) => {
if (ok) {
return await goto(data.redirectPath);
}
});
}
});
</script>
<svetle:head>
<title>승인을 기다리고 있어요.</title>
</svetle:head>
<div class="flex h-full flex-col">
<TitleDiv>
<div class="flex flex-col gap-y-2">
<h1 class="text-3xl font-bold">승인을 기다리고 있어요.</h1>
<p>
회원님의 다른 디바이스에서 이 디바이스의 데이터 접근을 승인해야 서비스를 이용할 수 있어요.
</p>
</div>
<div class="my-4 flex flex-col gap-y-2">
<div>
<IconFingerprint class="mx-auto text-7xl" />
<p class="text-center text-xl font-bold text-primary-500">암호 키 지문</p>
</div>
<div class="rounded-2xl bg-gray-100 p-4">
<p class="text-center text-2xl font-medium text-gray-800">
{#if !fingerprint}
지문 생성하는 중...
{:else}
{#await fingerprint}
지문 생성하는 중...
{:then fingerprint}
{fingerprint}
{/await}
{/if}
</p>
</div>
<p class="text-center">
암호 키 지문은 디바이스마다 다르게 생성돼요. <br />
지문이 일치하는지 확인 후 승인해 주세요.
</p>
</div>
</TitleDiv>
</div>

View File

@@ -0,0 +1,6 @@
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ url }) => {
const redirectPath = url.searchParams.get("redirect") || "/";
return { redirectPath };
};

View File

@@ -0,0 +1,53 @@
import { callAPI } from "$lib/hooks";
import { storeMasterKeys } from "$lib/indexedDB";
import {
decodeFromBase64,
exportRSAKey,
makeAESKeyNonextractable,
unwrapAESKeyUsingRSA,
digestSHA256,
} from "$lib/modules/crypto";
import { masterKeyStore } from "$lib/stores";
export const generateEncryptKeyFingerprint = async (encryptKey: CryptoKey) => {
const { key } = await exportRSAKey(encryptKey);
const digest = await digestSHA256(key);
return Array.from(new Uint8Array(digest))
.map((byte) => byte.toString(16).padStart(2, "0"))
.join("")
.toUpperCase()
.match(/.{1,4}/g)!
.join(" ");
};
export const requestMasterKeyDownload = async (decryptKey: CryptoKey) => {
const res = await callAPI("/api/mek/list", { method: "GET" });
if (!res.ok) return false;
const data = await res.json();
const { meks: masterKeysWrapped } = data as {
meks: {
version: number;
state: "active" | "retired";
mek: string;
}[];
};
const masterKeys = await Promise.all(
masterKeysWrapped.map(async ({ version, state, mek: masterKeyWrapped }) => ({
version,
state,
masterKey: await makeAESKeyNonextractable(
await unwrapAESKeyUsingRSA(decodeFromBase64(masterKeyWrapped), decryptKey),
),
})),
);
await storeMasterKeys(
masterKeys.map(({ version, state, masterKey }) => ({ version, state, key: masterKey })),
);
masterKeyStore.set(
new Map(masterKeys.map(({ version, state, masterKey }) => [version, { state, masterKey }])),
);
return true;
};