암호 키 생성 페이지에서 검증키와 서명키를 함께 생성하도록 변경

This commit is contained in:
static
2024-12-31 04:18:34 +09:00
parent a64e85848c
commit 0ef252913a
10 changed files with 225 additions and 82 deletions

View File

@@ -4,8 +4,14 @@ type Path = "/key/export";
interface KeyExportState { interface KeyExportState {
redirectPath: string; redirectPath: string;
pubKeyBase64: string; encKeyPair: {
privKeyBase64: string; pubKeyBase64: string;
privKeyBase64: string;
};
sigKeyPair: {
pubKeyBase64: string;
privKeyBase64: string;
};
mekDraft: ArrayBuffer; mekDraft: ArrayBuffer;
} }

View File

@@ -1,33 +1,31 @@
import { Dexie, type EntityTable } from "dexie"; import { Dexie, type EntityTable } from "dexie";
interface KeyPair { type RSAKeyUsage = "encrypt" | "decrypt" | "sign" | "verify";
type: "publicKey" | "privateKey";
interface RSAKey {
usage: RSAKeyUsage;
key: CryptoKey; key: CryptoKey;
} }
const keyStore = new Dexie("keyStore") as Dexie & { const keyStore = new Dexie("keyStore") as Dexie & {
keyPair: EntityTable<KeyPair, "type">; rsaKey: EntityTable<RSAKey, "usage">;
}; };
keyStore.version(1).stores({ keyStore.version(1).stores({
keyPair: "type", rsaKey: "usage, key",
}); });
export const getKeyPairFromIndexedDB = async () => { export const getRSAKey = async (usage: RSAKeyUsage) => {
const pubKey = await keyStore.keyPair.get("publicKey"); const key = await keyStore.rsaKey.get(usage);
const privKey = await keyStore.keyPair.get("privateKey"); return key?.key ?? null;
return {
pubKey: pubKey?.key ?? null,
privKey: privKey?.key ?? null,
};
}; };
export const storeKeyPairIntoIndexedDB = async (pubKey: CryptoKey, privKey: CryptoKey) => { export const storeRSAKey = async (key: CryptoKey, usage: RSAKeyUsage) => {
if (!pubKey.extractable) throw new Error("Public key must be extractable"); if ((usage === "encrypt" || usage === "verify") && !key.extractable) {
if (privKey.extractable) throw new Error("Private key must be non-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([ await keyStore.rsaKey.put({ usage, key });
{ type: "publicKey", key: pubKey },
{ type: "privateKey", key: privKey },
]);
}; };

View File

@@ -8,7 +8,7 @@ export const decodeFromBase64 = (data: string) => {
return Uint8Array.from(atob(data), (c) => c.charCodeAt(0)).buffer; 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( const keyPair = await window.crypto.subtle.generateKey(
{ {
name: "RSA-OAEP", name: "RSA-OAEP",
@@ -22,6 +22,20 @@ export const generateRSAKeyPair = async () => {
return keyPair; 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) => { export const makeRSAKeyNonextractable = async (key: CryptoKey, type: RSAKeyType) => {
const { format, key: exportedKey } = await exportRSAKey(key, type); const { format, key: exportedKey } = await exportRSAKey(key, type);
return await window.crypto.subtle.importKey( 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 () => { export const generateAESKey = async () => {
return await window.crypto.subtle.generateKey( return await window.crypto.subtle.generateKey(
{ {

View File

@@ -1,4 +1,9 @@
import { writable } from "svelte/store"; import { writable } from "svelte/store";
export const keyPairStore = writable<CryptoKeyPair | null>(null); interface KeyPairs {
encKeyPair: CryptoKeyPair;
sigKeyPair: CryptoKeyPair;
}
export const keyPairsStore = writable<KeyPairs | null>(null);
export const mekStore = writable<Map<number, CryptoKey>>(new Map()); export const mekStore = writable<Map<number, CryptoKey>>(new Map());

View File

@@ -3,13 +3,13 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { Button, TextButton } from "$lib/components/buttons"; import { Button, TextButton } from "$lib/components/buttons";
import { BottomDiv } from "$lib/components/divs"; import { BottomDiv } from "$lib/components/divs";
import { keyPairStore } from "$lib/stores"; import { keyPairsStore } from "$lib/stores";
import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte"; import BeforeContinueBottomSheet from "./BeforeContinueBottomSheet.svelte";
import BeforeContinueModal from "./BeforeContinueModal.svelte"; import BeforeContinueModal from "./BeforeContinueModal.svelte";
import { import {
createBlobFromKeyPairBase64, makeKeyPairsSaveable,
requestPubKeyRegistration, requestClientRegistration,
storeKeyPairPersistently, storeKeyPairsPersistently,
requestTokenUpgrade, requestTokenUpgrade,
requestInitialMekRegistration, requestInitialMekRegistration,
} from "./service"; } from "./service";
@@ -22,8 +22,9 @@
let isBeforeContinueBottomSheetOpen = $state(false); let isBeforeContinueBottomSheetOpen = $state(false);
const exportKeyPair = () => { const exportKeyPair = () => {
const keyPairBlob = createBlobFromKeyPairBase64(data.pubKeyBase64, data.privKeyBase64); const keyPairsSaveable = makeKeyPairsSaveable(data.encKeyPair, data.sigKeyPair);
saveAs(keyPairBlob, "arkvalut-keypair.pem"); const keyPairsBlob = new Blob([JSON.stringify(keyPairsSaveable)], { type: "application/json" });
saveAs(keyPairsBlob, "arkvalut-key.json");
if (!isBeforeContinueBottomSheetOpen) { if (!isBeforeContinueBottomSheetOpen) {
setTimeout(() => { setTimeout(() => {
@@ -33,7 +34,7 @@
}; };
const registerPubKey = async () => { const registerPubKey = async () => {
if (!$keyPairStore) { if (!$keyPairsStore) {
throw new Error("Failed to find key pair"); throw new Error("Failed to find key pair");
} }
@@ -41,15 +42,31 @@
isBeforeContinueBottomSheetOpen = false; isBeforeContinueBottomSheetOpen = false;
try { 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"); 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"); 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"); throw new Error("Failed to register initial MEK");
await goto(data.redirectPath); await goto(data.redirectPath);

View File

@@ -1,59 +1,117 @@
import { callAPI } from "$lib/hooks"; import { callAPI } from "$lib/hooks";
import { storeKeyPairIntoIndexedDB } from "$lib/indexedDB"; import { storeRSAKey } from "$lib/indexedDB";
import { import {
encodeToBase64, encodeToBase64,
decodeFromBase64, decodeFromBase64,
encryptRSAPlaintext, encryptRSAPlaintext,
decryptRSACiphertext, decryptRSACiphertext,
signRSAMessage,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
export const createBlobFromKeyPairBase64 = (pubKeyBase64: string, privKeyBase64: string) => { type ExportedKeyPairs = {
const pubKeyFormatted = pubKeyBase64.match(/.{1,64}/g)?.join("\n"); generator: "ArkVault";
const privKeyFormatted = privKeyBase64.match(/.{1,64}/g)?.join("\n"); exportedAt: Date;
if (!pubKeyFormatted || !privKeyFormatted) { } & {
throw new Error("Failed to format key pair"); version: 1;
} encKeyPair: { pubKey: string; privKey: string };
sigKeyPair: { pubKey: string; privKey: string };
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" });
}; };
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", { let res = await callAPI("/api/client/register", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ pubKey: pubKeyBase64 }), body: JSON.stringify({
encPubKey: encPubKeyBase64,
sigPubKey: sigPubKeyBase64,
}),
}); });
if (!res.ok) return false; if (!res.ok) return false;
const data = await res.json(); const { challenge } = await res.json();
const challenge = data.challenge as string; const answer = await decryptRSACiphertext(decodeFromBase64(challenge), encPrivKey);
const answer = await decryptRSACiphertext(decodeFromBase64(challenge), privateKey); const sigAnswer = await signRSAMessage(answer, sigPrivKey);
res = await callAPI("/api/client/verify", { res = await callAPI("/api/client/verify", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
}, },
body: JSON.stringify({ answer: encodeToBase64(answer) }), body: JSON.stringify({
answer: encodeToBase64(answer),
sigAnswer: encodeToBase64(sigAnswer),
}),
}); });
return res.ok; return res.ok;
}; };
export const storeKeyPairPersistently = async (keyPair: CryptoKeyPair) => { export const storeKeyPairsPersistently = async (
await storeKeyPairIntoIndexedDB(keyPair.publicKey, keyPair.privateKey); 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) => { export const requestTokenUpgrade = async (
const res = await fetch("/api/auth/upgradeToken", { encPubKeyBase64: string,
encPrivKey: CryptoKey,
sigPubKeyBase64: string,
sigPrivKey: CryptoKey,
) => {
let res = await fetch("/api/auth/upgradeToken", {
method: "POST", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "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; return res.ok;
}; };

View File

@@ -3,14 +3,15 @@
import { Button, TextButton } from "$lib/components/buttons"; import { Button, TextButton } from "$lib/components/buttons";
import { TitleDiv, BottomDiv } from "$lib/components/divs"; import { TitleDiv, BottomDiv } from "$lib/components/divs";
import { gotoStateful } from "$lib/hooks"; import { gotoStateful } from "$lib/hooks";
import { keyPairStore } from "$lib/stores"; import { keyPairsStore } from "$lib/stores";
import Order from "./Order.svelte"; import Order from "./Order.svelte";
import { generateKeyPair, generateMekDraft } from "./service"; import { generateKeyPairs, generateMekDraft } from "./service";
import IconKey from "~icons/material-symbols/key"; import IconKey from "~icons/material-symbols/key";
let { data } = $props(); let { data } = $props();
// TODO: Update
const orders = [ const orders = [
{ {
title: "암호 키는 공개 키와 개인 키로 구성돼요.", title: "암호 키는 공개 키와 개인 키로 구성돼요.",
@@ -33,19 +34,19 @@
const generate = async () => { const generate = async () => {
// TODO: Loading indicator // TODO: Loading indicator
const { pubKeyBase64, privKeyBase64 } = await generateKeyPair(); const { encKeyPair, sigKeyPair } = await generateKeyPairs();
const { mekDraft } = await generateMekDraft(); const { mekDraft } = await generateMekDraft();
await gotoStateful("/key/export", { await gotoStateful("/key/export", {
redirectPath: data.redirectPath, redirectPath: data.redirectPath,
pubKeyBase64, encKeyPair,
privKeyBase64, sigKeyPair,
mekDraft, mekDraft,
}); });
}; };
$effect(() => { $effect(() => {
if ($keyPairStore) { if ($keyPairsStore) {
goto(data.redirectPath); goto(data.redirectPath);
} }
}); });

View File

@@ -1,26 +1,43 @@
import { import {
encodeToBase64, encodeToBase64,
generateRSAKeyPair, generateRSAEncKeyPair,
generateRSASigKeyPair,
makeRSAKeyNonextractable, makeRSAKeyNonextractable,
exportRSAKey, exportRSAKey,
generateAESKey, generateAESKey,
makeAESKeyNonextractable, makeAESKeyNonextractable,
exportAESKey, exportAESKey,
} from "$lib/modules/crypto"; } from "$lib/modules/crypto";
import { keyPairStore, mekStore } from "$lib/stores"; import { keyPairsStore, mekStore } from "$lib/stores";
export const generateKeyPair = async () => { const exportRSAKeyToBase64 = async (key: CryptoKey, type: "public" | "private") => {
const keyPair = await generateRSAKeyPair(); return encodeToBase64((await exportRSAKey(key, type)).key);
const privKeySecured = await makeRSAKeyNonextractable(keyPair.privateKey, "private"); };
keyPairStore.set({ export const generateKeyPairs = async () => {
publicKey: keyPair.publicKey, const encKeyPair = await generateRSAEncKeyPair();
privateKey: privKeySecured, 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 { return {
pubKeyBase64: encodeToBase64((await exportRSAKey(keyPair.publicKey, "public")).key), encKeyPair: {
privKeyBase64: encodeToBase64((await exportRSAKey(keyPair.privateKey, "private")).key), 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"),
},
}; };
}; };

View File

@@ -1,21 +1,19 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { getKeyPairFromIndexedDB } from "$lib/indexedDB";
import { keyPairStore } from "$lib/stores";
import "../app.css"; import "../app.css";
import { prepareKeyPairStores } from "./services";
let { children } = $props(); let { children } = $props();
onMount(async () => { onMount(() => {
const { pubKey, privKey } = await getKeyPairFromIndexedDB(); prepareKeyPairStores().then(async (ok) => {
if (pubKey && privKey) { if (!ok && !["/auth", "/key"].some((path) => location.pathname.startsWith(path))) {
keyPairStore.set({ publicKey: pubKey, privateKey: privKey }); await goto(
} else if (!["/auth", "/key/generate"].some((path) => location.pathname.startsWith(path))) { "/key/generate?redirect=" + encodeURIComponent(location.pathname + location.search),
await goto( );
"/key/generate?redirect=" + encodeURIComponent(location.pathname + location.search), }
); });
}
}); });
</script> </script>

18
src/routes/services.ts Normal file
View File

@@ -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;
}
};