From 59c8523e25d10f1d0202ef60f72340e3a24c4e67 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 12 Jan 2025 21:52:41 +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=EB=B0=8F=20=EB=93=B1=EB=A1=9D=EC=8B=9C=20HSK?= =?UTF-8?q?=EB=8F=84=20=ED=95=A8=EA=BB=98=20=EC=83=9D=EC=84=B1=20=EB=B0=8F?= =?UTF-8?q?=20=EB=93=B1=EB=A1=9D=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/hooks.client.ts | 15 +++++-- src/lib/hooks/gotoStateful.ts | 1 + src/lib/indexedDB.ts | 23 ++++++++-- src/lib/modules/crypto/aes.ts | 21 +++++++++ src/lib/modules/crypto/rsa.ts | 8 ++-- src/lib/modules/crypto/sha.ts | 17 +++++++ src/lib/services/auth.ts | 4 +- src/lib/services/key.ts | 4 +- src/lib/stores/key.ts | 8 ++++ .../(fullscreen)/key/export/+page.svelte | 10 +++-- src/routes/(fullscreen)/key/export/service.ts | 20 +++++++-- .../(fullscreen)/key/generate/+page.svelte | 10 ++++- .../(fullscreen)/key/generate/service.ts | 13 +++++- .../(main)/directory/[[id]]/+page.svelte | 18 ++++++-- src/routes/(main)/directory/[[id]]/service.ts | 44 +++++++++++++++++-- 15 files changed, 183 insertions(+), 33 deletions(-) diff --git a/src/hooks.client.ts b/src/hooks.client.ts index 217d7ea..3f0ccfb 100644 --- a/src/hooks.client.ts +++ b/src/hooks.client.ts @@ -1,6 +1,6 @@ import type { ClientInit } from "@sveltejs/kit"; -import { getClientKey, getMasterKeys } from "$lib/indexedDB"; -import { clientKeyStore, masterKeyStore } from "$lib/stores"; +import { getClientKey, getMasterKeys, getHmacSecrets } from "$lib/indexedDB"; +import { clientKeyStore, masterKeyStore, hmacSecretStore } from "$lib/stores"; const prepareClientKeyStore = async () => { const [encryptKey, decryptKey, signKey, verifyKey] = await Promise.all([ @@ -21,6 +21,13 @@ const prepareMasterKeyStore = async () => { } }; -export const init: ClientInit = async () => { - await Promise.all([prepareClientKeyStore(), prepareMasterKeyStore()]); +const prepareHmacSecretStore = async () => { + const hmacSecrets = await getHmacSecrets(); + if (hmacSecrets.length > 0) { + hmacSecretStore.set(new Map(hmacSecrets.map((hmacSecret) => [hmacSecret.version, hmacSecret]))); + } +}; + +export const init: ClientInit = async () => { + await Promise.all([prepareClientKeyStore(), prepareMasterKeyStore(), prepareHmacSecretStore()]); }; diff --git a/src/lib/hooks/gotoStateful.ts b/src/lib/hooks/gotoStateful.ts index fffc95f..4bcbebf 100644 --- a/src/lib/hooks/gotoStateful.ts +++ b/src/lib/hooks/gotoStateful.ts @@ -11,6 +11,7 @@ interface KeyExportState { verifyKeyBase64: string; masterKeyWrapped: string; + hmacSecretWrapped: string; } const useAutoNull = (value: T | null) => { diff --git a/src/lib/indexedDB.ts b/src/lib/indexedDB.ts index b2fbd22..7a4c89e 100644 --- a/src/lib/indexedDB.ts +++ b/src/lib/indexedDB.ts @@ -7,22 +7,28 @@ interface ClientKey { key: CryptoKey; } -type MasterKeyState = "active" | "retired"; - interface MasterKey { version: number; - state: MasterKeyState; + state: "active" | "retired"; key: CryptoKey; } +interface HmacSecret { + version: number; + state: "active"; + secret: CryptoKey; +} + const keyStore = new Dexie("keyStore") as Dexie & { clientKey: EntityTable; masterKey: EntityTable; + hmacSecret: EntityTable; }; keyStore.version(1).stores({ clientKey: "usage", masterKey: "version", + hmacSecret: "version", }); export const getClientKey = async (usage: ClientKeyUsage) => { @@ -62,3 +68,14 @@ export const storeMasterKeys = async (keys: MasterKey[]) => { } await keyStore.masterKey.bulkPut(keys); }; + +export const getHmacSecrets = async () => { + return await keyStore.hmacSecret.toArray(); +}; + +export const storeHmacSecrets = async (secrets: HmacSecret[]) => { + if (secrets.some(({ secret }) => secret.extractable)) { + throw new Error("Hmac secrets must be nonextractable"); + } + await keyStore.hmacSecret.bulkPut(secrets); +}; diff --git a/src/lib/modules/crypto/aes.ts b/src/lib/modules/crypto/aes.ts index df04851..3c096ba 100644 --- a/src/lib/modules/crypto/aes.ts +++ b/src/lib/modules/crypto/aes.ts @@ -55,6 +55,27 @@ export const unwrapDataKey = async (dataKeyWrapped: string, masterKey: CryptoKey }; }; +export const wrapHmacSecret = async (hmacSecret: CryptoKey, masterKey: CryptoKey) => { + return encodeToBase64(await window.crypto.subtle.wrapKey("raw", hmacSecret, masterKey, "AES-KW")); +}; + +export const unwrapHmacSecret = async (hmacSecretWrapped: string, masterKey: CryptoKey) => { + return { + hmacSecret: await window.crypto.subtle.unwrapKey( + "raw", + decodeFromBase64(hmacSecretWrapped), + masterKey, + "AES-KW", + { + name: "HMAC", + hash: "SHA-256", + } satisfies HmacImportParams, + false, // Nonextractable + ["sign", "verify"], + ), + }; +}; + export const encryptData = async (data: BufferSource, dataKey: CryptoKey) => { const iv = window.crypto.getRandomValues(new Uint8Array(12)); const ciphertext = await window.crypto.subtle.encrypt( diff --git a/src/lib/modules/crypto/rsa.ts b/src/lib/modules/crypto/rsa.ts index 9eb81c0..4df8f9e 100644 --- a/src/lib/modules/crypto/rsa.ts +++ b/src/lib/modules/crypto/rsa.ts @@ -95,7 +95,7 @@ export const unwrapMasterKey = async ( }; }; -export const signMessage = async (message: BufferSource, signKey: CryptoKey) => { +export const signMessageRSA = async (message: BufferSource, signKey: CryptoKey) => { return await window.crypto.subtle.sign( { name: "RSA-PSS", @@ -106,7 +106,7 @@ export const signMessage = async (message: BufferSource, signKey: CryptoKey) => ); }; -export const verifySignature = async ( +export const verifySignatureRSA = async ( message: BufferSource, signature: BufferSource, verifyKey: CryptoKey, @@ -131,7 +131,7 @@ export const signMasterKeyWrapped = async ( version: masterKeyVersion, key: masterKeyWrapped, }); - return encodeToBase64(await signMessage(encodeString(serialized), signKey)); + return encodeToBase64(await signMessageRSA(encodeString(serialized), signKey)); }; export const verifyMasterKeyWrapped = async ( @@ -144,7 +144,7 @@ export const verifyMasterKeyWrapped = async ( version: masterKeyVersion, key: masterKeyWrapped, }); - return await verifySignature( + return await verifySignatureRSA( encodeString(serialized), decodeFromBase64(masterKeyWrappedSig), verifyKey, diff --git a/src/lib/modules/crypto/sha.ts b/src/lib/modules/crypto/sha.ts index e79f706..3acb258 100644 --- a/src/lib/modules/crypto/sha.ts +++ b/src/lib/modules/crypto/sha.ts @@ -1,3 +1,20 @@ export const digestMessage = async (message: BufferSource) => { return await window.crypto.subtle.digest("SHA-256", message); }; + +export const generateHmacSecret = async () => { + return { + hmacSecret: await window.crypto.subtle.generateKey( + { + name: "HMAC", + hash: "SHA-256", + } satisfies HmacKeyGenParams, + true, + ["sign", "verify"], + ), + }; +}; + +export const signMessageHmac = async (message: BufferSource, hmacSecret: CryptoKey) => { + return await window.crypto.subtle.sign("HMAC", hmacSecret, message); +}; diff --git a/src/lib/services/auth.ts b/src/lib/services/auth.ts index 03d445a..498c794 100644 --- a/src/lib/services/auth.ts +++ b/src/lib/services/auth.ts @@ -1,5 +1,5 @@ import { callPostApi } from "$lib/hooks"; -import { encodeToBase64, decryptChallenge, signMessage } from "$lib/modules/crypto"; +import { encodeToBase64, decryptChallenge, signMessageRSA } from "$lib/modules/crypto"; import type { SessionUpgradeRequest, SessionUpgradeResponse, @@ -20,7 +20,7 @@ export const requestSessionUpgrade = async ( const { challenge }: SessionUpgradeResponse = await res.json(); const answer = await decryptChallenge(challenge, decryptKey); - const answerSig = await signMessage(answer, signKey); + const answerSig = await signMessageRSA(answer, signKey); res = await callPostApi("/api/auth/upgradeSession/verify", { answer: encodeToBase64(answer), diff --git a/src/lib/services/key.ts b/src/lib/services/key.ts index 79a4390..fb368dd 100644 --- a/src/lib/services/key.ts +++ b/src/lib/services/key.ts @@ -3,7 +3,7 @@ import { storeMasterKeys } from "$lib/indexedDB"; import { encodeToBase64, decryptChallenge, - signMessage, + signMessageRSA, unwrapMasterKey, verifyMasterKeyWrapped, } from "$lib/modules/crypto"; @@ -29,7 +29,7 @@ export const requestClientRegistration = async ( const { challenge }: ClientRegisterResponse = await res.json(); const answer = await decryptChallenge(challenge, decryptKey); - const answerSig = await signMessage(answer, signKey); + const answerSig = await signMessageRSA(answer, signKey); res = await callPostApi("/api/client/register/verify", { answer: encodeToBase64(answer), diff --git a/src/lib/stores/key.ts b/src/lib/stores/key.ts index d742634..77d1268 100644 --- a/src/lib/stores/key.ts +++ b/src/lib/stores/key.ts @@ -13,6 +13,14 @@ export interface MasterKey { key: CryptoKey; } +export interface HmacSecret { + version: number; + state: "active"; + secret: CryptoKey; +} + export const clientKeyStore = writable(null); export const masterKeyStore = writable | null>(null); + +export const hmacSecretStore = writable | null>(null); diff --git a/src/routes/(fullscreen)/key/export/+page.svelte b/src/routes/(fullscreen)/key/export/+page.svelte index 3ce209f..297d91d 100644 --- a/src/routes/(fullscreen)/key/export/+page.svelte +++ b/src/routes/(fullscreen)/key/export/+page.svelte @@ -11,7 +11,7 @@ requestClientRegistration, storeClientKeys, requestSessionUpgrade, - requestInitialMasterKeyRegistration, + requestInitialMasterKeyAndHmacSecretRegistration, } from "./service"; import IconKey from "~icons/material-symbols/key"; @@ -69,9 +69,13 @@ throw new Error("Failed to upgrade session"); if ( - !(await requestInitialMasterKeyRegistration(data.masterKeyWrapped, $clientKeyStore.signKey)) + !(await requestInitialMasterKeyAndHmacSecretRegistration( + data.masterKeyWrapped, + data.hmacSecretWrapped, + $clientKeyStore.signKey, + )) ) - throw new Error("Failed to register initial MEK"); + throw new Error("Failed to register initial MEK and HSK"); await goto("/client/pending?redirect=" + encodeURIComponent(data.redirectPath)); } catch (e) { diff --git a/src/routes/(fullscreen)/key/export/service.ts b/src/routes/(fullscreen)/key/export/service.ts index b02dd09..a9aaaee 100644 --- a/src/routes/(fullscreen)/key/export/service.ts +++ b/src/routes/(fullscreen)/key/export/service.ts @@ -1,7 +1,10 @@ import { callPostApi } from "$lib/hooks"; import { storeClientKey } from "$lib/indexedDB"; import { signMasterKeyWrapped } from "$lib/modules/crypto"; -import type { InitialMasterKeyRegisterRequest } from "$lib/server/schemas"; +import type { + InitialMasterKeyRegisterRequest, + InitialHmacSecretRegisterRequest, +} from "$lib/server/schemas"; import type { ClientKeys } from "$lib/stores"; export { requestSessionUpgrade } from "$lib/services/auth"; @@ -44,13 +47,22 @@ export const storeClientKeys = async (clientKeys: ClientKeys) => { ]); }; -export const requestInitialMasterKeyRegistration = async ( +export const requestInitialMasterKeyAndHmacSecretRegistration = async ( masterKeyWrapped: string, + hmacSecretWrapped: string, signKey: CryptoKey, ) => { - const res = await callPostApi("/api/mek/register/initial", { + let res = await callPostApi("/api/mek/register/initial", { mek: masterKeyWrapped, mekSig: await signMasterKeyWrapped(masterKeyWrapped, 1, signKey), }); - return res.ok || res.status === 409; + if (!res.ok) { + return res.status === 409; + } + + res = await callPostApi("/api/hsk/register/initial", { + mekVersion: 1, + hsk: hmacSecretWrapped, + }); + return res.ok; }; diff --git a/src/routes/(fullscreen)/key/generate/+page.svelte b/src/routes/(fullscreen)/key/generate/+page.svelte index 81c6180..330a39e 100644 --- a/src/routes/(fullscreen)/key/generate/+page.svelte +++ b/src/routes/(fullscreen)/key/generate/+page.svelte @@ -6,7 +6,11 @@ import { gotoStateful } from "$lib/hooks"; import { clientKeyStore } from "$lib/stores"; import Order from "./Order.svelte"; - import { generateClientKeys, generateInitialMasterKey } from "./service"; + import { + generateClientKeys, + generateInitialMasterKey, + generateInitialHmacSecret, + } from "./service"; import IconKey from "~icons/material-symbols/key"; @@ -36,12 +40,14 @@ // TODO: Loading indicator const { encryptKey, ...clientKeys } = await generateClientKeys(); - const { masterKeyWrapped } = await generateInitialMasterKey(encryptKey); + const { masterKey, masterKeyWrapped } = await generateInitialMasterKey(encryptKey); + const { hmacSecretWrapped } = await generateInitialHmacSecret(masterKey); await gotoStateful("/key/export", { ...clientKeys, redirectPath: data.redirectPath, masterKeyWrapped, + hmacSecretWrapped, }); }; diff --git a/src/routes/(fullscreen)/key/generate/service.ts b/src/routes/(fullscreen)/key/generate/service.ts index b63da21..f970f46 100644 --- a/src/routes/(fullscreen)/key/generate/service.ts +++ b/src/routes/(fullscreen)/key/generate/service.ts @@ -3,8 +3,11 @@ import { generateSigningKeyPair, exportRSAKeyToBase64, makeRSAKeyNonextractable, - generateMasterKey, wrapMasterKey, + generateMasterKey, + makeAESKeyNonextractable, + wrapHmacSecret, + generateHmacSecret, } from "$lib/modules/crypto"; import { clientKeyStore } from "$lib/stores"; @@ -31,6 +34,14 @@ export const generateClientKeys = async () => { export const generateInitialMasterKey = async (encryptKey: CryptoKey) => { const { masterKey } = await generateMasterKey(); return { + masterKey: await makeAESKeyNonextractable(masterKey), masterKeyWrapped: await wrapMasterKey(masterKey, encryptKey), }; }; + +export const generateInitialHmacSecret = async (masterKey: CryptoKey) => { + const { hmacSecret } = await generateHmacSecret(); + return { + hmacSecretWrapped: await wrapHmacSecret(hmacSecret, masterKey), + }; +}; diff --git a/src/routes/(main)/directory/[[id]]/+page.svelte b/src/routes/(main)/directory/[[id]]/+page.svelte index f7fc3d4..a868e8a 100644 --- a/src/routes/(main)/directory/[[id]]/+page.svelte +++ b/src/routes/(main)/directory/[[id]]/+page.svelte @@ -1,10 +1,11 @@