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

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 {
redirectPath: string;
pubKeyBase64: string;
privKeyBase64: string;
encKeyPair: {
pubKeyBase64: string;
privKeyBase64: string;
};
sigKeyPair: {
pubKeyBase64: string;
privKeyBase64: string;
};
mekDraft: ArrayBuffer;
}

View File

@@ -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<KeyPair, "type">;
rsaKey: EntityTable<RSAKey, "usage">;
};
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 });
};

View File

@@ -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(
{

View File

@@ -1,4 +1,9 @@
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());

View File

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

View File

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

View File

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

View File

@@ -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"),
},
};
};

View File

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