암호 키 생성 및 등록시 HSK도 함께 생성 및 등록하도록 변경

This commit is contained in:
static
2025-01-12 21:52:41 +09:00
parent 805d7df182
commit 59c8523e25
15 changed files with 183 additions and 33 deletions

View File

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

View File

@@ -11,6 +11,7 @@ interface KeyExportState {
verifyKeyBase64: string;
masterKeyWrapped: string;
hmacSecretWrapped: string;
}
const useAutoNull = <T>(value: T | null) => {

View File

@@ -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<ClientKey, "usage">;
masterKey: EntityTable<MasterKey, "version">;
hmacSecret: EntityTable<HmacSecret, "version">;
};
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);
};

View File

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

View File

@@ -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,

View File

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

View File

@@ -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<SessionUpgradeVerifyRequest>("/api/auth/upgradeSession/verify", {
answer: encodeToBase64(answer),

View File

@@ -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<ClientRegisterVerifyRequest>("/api/client/register/verify", {
answer: encodeToBase64(answer),

View File

@@ -13,6 +13,14 @@ export interface MasterKey {
key: CryptoKey;
}
export interface HmacSecret {
version: number;
state: "active";
secret: CryptoKey;
}
export const clientKeyStore = writable<ClientKeys | null>(null);
export const masterKeyStore = writable<Map<number, MasterKey> | null>(null);
export const hmacSecretStore = writable<Map<number, HmacSecret> | null>(null);

View File

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

View File

@@ -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<InitialMasterKeyRegisterRequest>("/api/mek/register/initial", {
let res = await callPostApi<InitialMasterKeyRegisterRequest>("/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<InitialHmacSecretRegisterRequest>("/api/hsk/register/initial", {
mekVersion: 1,
hsk: hmacSecretWrapped,
});
return res.ok;
};

View File

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

View File

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

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import { onMount } from "svelte";
import type { Writable } from "svelte/store";
import { goto } from "$app/navigation";
import { TopBar } from "$lib/components";
import { FloatingButton } from "$lib/components/buttons";
import { getDirectoryInfo } from "$lib/modules/file";
import { masterKeyStore, type DirectoryInfo } from "$lib/stores";
import { masterKeyStore, hmacSecretStore, type DirectoryInfo } from "$lib/stores";
import CreateBottomSheet from "./CreateBottomSheet.svelte";
import CreateDirectoryModal from "./CreateDirectoryModal.svelte";
import DeleteDirectoryEntryModal from "./DeleteDirectoryEntryModal.svelte";
@@ -12,6 +13,7 @@
import DirectoryEntryMenuBottomSheet from "./DirectoryEntryMenuBottomSheet.svelte";
import RenameDirectoryEntryModal from "./RenameDirectoryEntryModal.svelte";
import {
requestHmacSecretDownload,
requestDirectoryCreation,
requestFileUpload,
requestDirectoryEntryRename,
@@ -44,11 +46,19 @@
const file = fileInput?.files?.[0];
if (!file) return;
requestFileUpload(file, data.id, $masterKeyStore?.get(1)!).then(() => {
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
});
requestFileUpload(file, data.id, $masterKeyStore?.get(1)!, $hmacSecretStore?.get(1)!).then(
() => {
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!); // TODO: FIXME
},
);
};
onMount(async () => {
if (!$hmacSecretStore && !(await requestHmacSecretDownload($masterKeyStore?.get(1)?.key!))) {
throw new Error("Failed to download hmac secrets");
}
});
$effect(() => {
info = getDirectoryInfo(data.id, $masterKeyStore?.get(1)?.key!);
});

View File

@@ -1,12 +1,22 @@
import { callPostApi } from "$lib/hooks";
import { generateDataKey, wrapDataKey, encryptData, encryptString } from "$lib/modules/crypto";
import { callGetApi, callPostApi } from "$lib/hooks";
import { storeHmacSecrets } from "$lib/indexedDB";
import {
encodeToBase64,
generateDataKey,
wrapDataKey,
unwrapHmacSecret,
encryptData,
encryptString,
signMessageHmac,
} from "$lib/modules/crypto";
import type {
DirectoryRenameRequest,
DirectoryCreateRequest,
FileRenameRequest,
FileUploadRequest,
HmacSecretListResponse,
} from "$lib/server/schemas";
import type { MasterKey } from "$lib/stores";
import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores";
export interface SelectedDirectoryEntry {
type: "directory" | "file";
@@ -16,6 +26,26 @@ export interface SelectedDirectoryEntry {
name: string;
}
export const requestHmacSecretDownload = async (masterKey: CryptoKey) => {
// TODO: MEK rotation
const res = await callGetApi("/api/hsk/list");
if (!res.ok) return false;
const { hsks: hmacSecretsWrapped }: HmacSecretListResponse = await res.json();
const hmacSecrets = await Promise.all(
hmacSecretsWrapped.map(async ({ version, state, hsk: hmacSecretWrapped }) => {
const { hmacSecret } = await unwrapHmacSecret(hmacSecretWrapped, masterKey);
return { version, state, secret: hmacSecret };
}),
);
await storeHmacSecrets(hmacSecrets);
hmacSecretStore.set(new Map(hmacSecrets.map((hmacSecret) => [hmacSecret.version, hmacSecret])));
return true;
};
export const requestDirectoryCreation = async (
name: string,
parentId: "root" | number,
@@ -37,11 +67,15 @@ export const requestFileUpload = async (
file: File,
parentId: "root" | number,
masterKey: MasterKey,
hmacSecret: HmacSecret,
) => {
const { dataKey, dataKeyVersion } = await generateDataKey();
const fileEncrypted = await encryptData(await file.arrayBuffer(), dataKey);
const nameEncrypted = await encryptString(file.name, dataKey);
const fileBuffer = await file.arrayBuffer();
const fileSigned = await signMessageHmac(fileBuffer, hmacSecret.secret);
const fileEncrypted = await encryptData(fileBuffer, dataKey);
const form = new FormData();
form.set(
"metadata",
@@ -50,6 +84,8 @@ export const requestFileUpload = async (
mekVersion: masterKey.version,
dek: await wrapDataKey(dataKey, masterKey.key),
dekVersion: dataKeyVersion.toISOString(),
hskVersion: hmacSecret.version,
contentHmac: encodeToBase64(fileSigned),
contentType: file.type,
contentIv: fileEncrypted.iv,
name: nameEncrypted.ciphertext,