mirror of
https://github.com/kmc7468/arkvault.git
synced 2025-12-12 21:08:46 +00:00
/api/mek/register, /api/mek/share Endpoint 삭제 및 MEK 서명 매커니즘 구현
2025년 첫 커밋! Happy New Year~
This commit is contained in:
@@ -76,6 +76,22 @@ export const signRSAMessage = async (message: BufferSource, privateKey: CryptoKe
|
||||
);
|
||||
};
|
||||
|
||||
export const verifyRSASignature = async (
|
||||
message: BufferSource,
|
||||
signature: BufferSource,
|
||||
publicKey: CryptoKey,
|
||||
) => {
|
||||
return await window.crypto.subtle.verify(
|
||||
{
|
||||
name: "RSA-PSS",
|
||||
saltLength: 32,
|
||||
} satisfies RsaPssParams,
|
||||
publicKey,
|
||||
signature,
|
||||
message,
|
||||
);
|
||||
};
|
||||
|
||||
export const generateAESKey = async () => {
|
||||
return await window.crypto.subtle.generateKey(
|
||||
{
|
||||
@@ -136,3 +152,24 @@ export const signRequest = async <T>(data: T, privateKey: CryptoKey) => {
|
||||
signature: encodeToBase64(signature),
|
||||
});
|
||||
};
|
||||
|
||||
export const signMasterKeyWrapped = async (
|
||||
version: number,
|
||||
masterKeyWrapped: ArrayBuffer,
|
||||
privateKey: CryptoKey,
|
||||
) => {
|
||||
const data = JSON.stringify({ version, key: encodeToBase64(masterKeyWrapped) });
|
||||
const dataBuffer = new TextEncoder().encode(data);
|
||||
return encodeToBase64(await signRSAMessage(dataBuffer, privateKey));
|
||||
};
|
||||
|
||||
export const verifyMasterKeyWrappedSig = async (
|
||||
version: number,
|
||||
masterKeyWrappedBase64: string,
|
||||
masterKeyWrappedSig: string,
|
||||
publicKey: CryptoKey,
|
||||
) => {
|
||||
const data = JSON.stringify({ version, key: masterKeyWrappedBase64 });
|
||||
const dataBuffer = new TextEncoder().encode(data);
|
||||
return await verifyRSASignature(dataBuffer, decodeFromBase64(masterKeyWrappedSig), publicKey);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { and, or, eq, lt, desc } from "drizzle-orm";
|
||||
import { and, or, eq } from "drizzle-orm";
|
||||
import db from "./drizzle";
|
||||
import { mek, clientMek, userClient } from "./schema";
|
||||
import { mek, clientMek } from "./schema";
|
||||
|
||||
export const registerInitialMek = async (userId: number, createdBy: number, encMek: string) => {
|
||||
export const registerInitialMek = async (
|
||||
userId: number,
|
||||
createdBy: number,
|
||||
encMek: string,
|
||||
encMekSig: string,
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.insert(mek).values({
|
||||
userId,
|
||||
@@ -16,70 +21,11 @@ export const registerInitialMek = async (userId: number, createdBy: number, encM
|
||||
clientId: createdBy,
|
||||
mekVersion: 1,
|
||||
encMek,
|
||||
encMekSig,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const registerActiveMek = async (
|
||||
userId: number,
|
||||
version: number,
|
||||
createdBy: number,
|
||||
clientMeks: {
|
||||
clientId: number;
|
||||
encMek: string;
|
||||
}[],
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
// 1. Check if the clientMeks are valid
|
||||
const userClients = await tx
|
||||
.select()
|
||||
.from(userClient)
|
||||
.where(and(eq(userClient.userId, userId), eq(userClient.state, "active")));
|
||||
if (
|
||||
clientMeks.length !== userClients.length ||
|
||||
!clientMeks.every((clientMek) =>
|
||||
userClients.some((userClient) => userClient.clientId === clientMek.clientId),
|
||||
)
|
||||
) {
|
||||
throw new Error("Invalid key list");
|
||||
}
|
||||
|
||||
// 2. Retire the old active MEK and insert the new one
|
||||
await tx
|
||||
.update(mek)
|
||||
.set({
|
||||
state: "retired",
|
||||
retiredAt: new Date(),
|
||||
})
|
||||
.where(and(eq(mek.userId, userId), lt(mek.version, version), eq(mek.state, "active")));
|
||||
await tx.insert(mek).values({
|
||||
userId,
|
||||
version,
|
||||
createdBy,
|
||||
createdAt: new Date(),
|
||||
state: "active",
|
||||
});
|
||||
|
||||
// 3. Insert the new client MEKs
|
||||
await tx.insert(clientMek).values(
|
||||
clientMeks.map(({ clientId, encMek }) => ({
|
||||
userId,
|
||||
clientId,
|
||||
mekVersion: version,
|
||||
encMek,
|
||||
})),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllValidMeks = async (userId: number) => {
|
||||
return await db
|
||||
.select()
|
||||
.from(mek)
|
||||
.where(and(eq(mek.userId, userId), or(eq(mek.state, "active"), eq(mek.state, "retired"))))
|
||||
.execute();
|
||||
};
|
||||
|
||||
export const getInitialMek = async (userId: number) => {
|
||||
const meks = await db
|
||||
.select()
|
||||
@@ -89,68 +35,6 @@ export const getInitialMek = async (userId: number) => {
|
||||
return meks[0] ?? null;
|
||||
};
|
||||
|
||||
export const getNextActiveMekVersion = async (userId: number) => {
|
||||
const meks = await db
|
||||
.select({ version: mek.version })
|
||||
.from(mek)
|
||||
.where(eq(mek.userId, userId))
|
||||
.orderBy(desc(mek.version))
|
||||
.limit(1)
|
||||
.execute();
|
||||
if (!meks[0]) {
|
||||
throw new Error("No MEK found");
|
||||
}
|
||||
return meks[0].version + 1;
|
||||
};
|
||||
|
||||
export const registerClientMeks = async (
|
||||
userId: number,
|
||||
clientId: number,
|
||||
clientMeks: {
|
||||
version: number;
|
||||
encMek: string;
|
||||
}[],
|
||||
) => {
|
||||
await db.transaction(async (tx) => {
|
||||
// 1. Check if the client is valid
|
||||
const userClients = await tx
|
||||
.select()
|
||||
.from(userClient)
|
||||
.where(
|
||||
and(
|
||||
eq(userClient.userId, userId),
|
||||
eq(userClient.clientId, clientId),
|
||||
eq(userClient.state, "active"),
|
||||
),
|
||||
);
|
||||
if (userClients.length === 0) {
|
||||
throw new Error("Invalid client");
|
||||
}
|
||||
|
||||
// 2. Check if the clientMeks are valid
|
||||
const meks = await tx
|
||||
.select()
|
||||
.from(mek)
|
||||
.where(and(eq(mek.userId, userId), or(eq(mek.state, "active"), eq(mek.state, "retired"))));
|
||||
if (
|
||||
clientMeks.length !== meks.length ||
|
||||
!clientMeks.every((clientMek) => meks.some((mek) => mek.version === clientMek.version))
|
||||
) {
|
||||
throw new Error("Invalid key list");
|
||||
}
|
||||
|
||||
// 3. Insert the client MEKs
|
||||
await tx.insert(clientMek).values(
|
||||
clientMeks.map(({ version, encMek }) => ({
|
||||
userId,
|
||||
clientId,
|
||||
mekVersion: version,
|
||||
encMek,
|
||||
})),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export const getAllValidClientMeks = async (userId: number, clientId: number) => {
|
||||
return await db
|
||||
.select()
|
||||
|
||||
@@ -30,8 +30,9 @@ export const clientMek = sqliteTable(
|
||||
clientId: integer("client_id")
|
||||
.notNull()
|
||||
.references(() => client.id),
|
||||
mekVersion: integer("master_encryption_key_version").notNull(),
|
||||
encMek: text("encrypted_master_encryption_key").notNull(),
|
||||
mekVersion: integer("version").notNull(),
|
||||
encMek: text("encrypted_key").notNull(), // Base64
|
||||
encMekSig: text("encrypted_key_signature").notNull(), // Base64
|
||||
},
|
||||
(t) => ({
|
||||
pk: primaryKey({ columns: [t.userId, t.clientId, t.mekVersion] }),
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { getUserClientWithDetails } from "$lib/server/db/client";
|
||||
import { getInitialMek } from "$lib/server/db/mek";
|
||||
import { verifySignature } from "$lib/server/modules/crypto";
|
||||
|
||||
export const isInitialMekNeeded = async (userId: number) => {
|
||||
const initialMek = await getInitialMek(userId);
|
||||
return !initialMek;
|
||||
};
|
||||
|
||||
export const verifyClientEncMekSig = async (
|
||||
userId: number,
|
||||
clientId: number,
|
||||
version: number,
|
||||
encMek: string,
|
||||
encMekSig: string,
|
||||
) => {
|
||||
const userClient = await getUserClientWithDetails(userId, clientId);
|
||||
if (!userClient) {
|
||||
error(500, "Invalid access token");
|
||||
}
|
||||
|
||||
const data = JSON.stringify({ version, key: encMek });
|
||||
return verifySignature(Buffer.from(data), encMekSig, userClient.client.sigPubKey);
|
||||
};
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
import { error } from "@sveltejs/kit";
|
||||
import { getAllUserClients, setUserClientStateToActive } from "$lib/server/db/client";
|
||||
import {
|
||||
registerInitialMek,
|
||||
registerActiveMek,
|
||||
getAllValidMeks,
|
||||
getNextActiveMekVersion,
|
||||
registerClientMeks,
|
||||
getAllValidClientMeks,
|
||||
} from "$lib/server/db/mek";
|
||||
import { isInitialMekNeeded } from "$lib/server/modules/mek";
|
||||
import { setUserClientStateToActive } from "$lib/server/db/client";
|
||||
import { registerInitialMek, getAllValidClientMeks } from "$lib/server/db/mek";
|
||||
import { isInitialMekNeeded, verifyClientEncMekSig } from "$lib/server/modules/mek";
|
||||
|
||||
export const getClientMekList = async (userId: number, clientId: number) => {
|
||||
const clientMeks = await getAllValidClientMeks(userId, clientId);
|
||||
@@ -17,6 +10,7 @@ export const getClientMekList = async (userId: number, clientId: number) => {
|
||||
version: clientMek.master_encryption_key.version,
|
||||
state: clientMek.master_encryption_key.state,
|
||||
mek: clientMek.client_master_encryption_key.encMek,
|
||||
mekSig: clientMek.client_master_encryption_key.encMekSig,
|
||||
})),
|
||||
};
|
||||
};
|
||||
@@ -25,53 +19,14 @@ export const registerInitialActiveMek = async (
|
||||
userId: number,
|
||||
createdBy: number,
|
||||
encMek: string,
|
||||
encMekSig: string,
|
||||
) => {
|
||||
if (!(await isInitialMekNeeded(userId))) {
|
||||
error(409, "Initial MEK already registered");
|
||||
} else if (!(await verifyClientEncMekSig(userId, createdBy, 1, encMek, encMekSig))) {
|
||||
error(400, "Invalid signature");
|
||||
}
|
||||
|
||||
await registerInitialMek(userId, createdBy, encMek);
|
||||
await registerInitialMek(userId, createdBy, encMek, encMekSig);
|
||||
await setUserClientStateToActive(userId, createdBy);
|
||||
};
|
||||
|
||||
export const registerNewActiveMek = async (
|
||||
userId: number,
|
||||
createdBy: number,
|
||||
clientMeks: {
|
||||
clientId: number;
|
||||
encMek: string;
|
||||
}[],
|
||||
) => {
|
||||
const userClients = await getAllUserClients(userId);
|
||||
const activeUserClients = userClients.filter(({ state }) => state === "active");
|
||||
if (
|
||||
clientMeks.length !== activeUserClients.length ||
|
||||
!clientMeks.every((clientMek) =>
|
||||
activeUserClients.some((userClient) => userClient.clientId === clientMek.clientId),
|
||||
)
|
||||
) {
|
||||
error(400, "Invalid key list");
|
||||
}
|
||||
|
||||
const newMekVersion = await getNextActiveMekVersion(userId);
|
||||
await registerActiveMek(userId, newMekVersion, createdBy, clientMeks);
|
||||
};
|
||||
|
||||
export const shareMeksForNewClient = async (
|
||||
userId: number,
|
||||
targetClientId: number,
|
||||
clientMeks: {
|
||||
version: number;
|
||||
encMek: string;
|
||||
}[],
|
||||
) => {
|
||||
const meks = await getAllValidMeks(userId);
|
||||
if (
|
||||
clientMeks.length !== meks.length ||
|
||||
!clientMeks.every((clientMek) => meks.some((mek) => mek.version === clientMek.version))
|
||||
) {
|
||||
error(400, "Invalid key list");
|
||||
}
|
||||
|
||||
await registerClientMeks(userId, targetClientId, clientMeks);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
signRSAMessage,
|
||||
makeAESKeyNonextractable,
|
||||
unwrapAESKeyUsingRSA,
|
||||
verifyMasterKeyWrappedSig,
|
||||
} from "$lib/modules/crypto";
|
||||
import { masterKeyStore } from "$lib/stores";
|
||||
|
||||
@@ -45,7 +46,7 @@ export const requestClientRegistration = async (
|
||||
return res.ok;
|
||||
};
|
||||
|
||||
export const requestMasterKeyDownload = async (decryptKey: CryptoKey) => {
|
||||
export const requestMasterKeyDownload = async (decryptKey: CryptoKey, verfiyKey: CryptoKey) => {
|
||||
const res = await callAPI("/api/mek/list", { method: "GET" });
|
||||
if (!res.ok) return false;
|
||||
|
||||
@@ -55,17 +56,28 @@ export const requestMasterKeyDownload = async (decryptKey: CryptoKey) => {
|
||||
version: number;
|
||||
state: "active" | "retired";
|
||||
mek: string;
|
||||
mekSig: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
const masterKeys = await Promise.all(
|
||||
masterKeysWrapped.map(async ({ version, state, mek: masterKeyWrapped }) => ({
|
||||
masterKeysWrapped.map(
|
||||
async ({ version, state, mek: masterKeyWrapped, mekSig: masterKeyWrappedSig }) => ({
|
||||
version,
|
||||
state,
|
||||
masterKey: await makeAESKeyNonextractable(
|
||||
await unwrapAESKeyUsingRSA(decodeFromBase64(masterKeyWrapped), decryptKey),
|
||||
),
|
||||
})),
|
||||
isValid: await verifyMasterKeyWrappedSig(
|
||||
version,
|
||||
masterKeyWrapped,
|
||||
masterKeyWrappedSig,
|
||||
verfiyKey,
|
||||
),
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (!masterKeys.every(({ isValid }) => isValid)) return false;
|
||||
|
||||
await storeMasterKeys(
|
||||
masterKeys.map(({ version, state, masterKey }) => ({ version, state, key: masterKey })),
|
||||
|
||||
@@ -29,7 +29,10 @@
|
||||
|
||||
// TODO: Multi-user support
|
||||
|
||||
if ($masterKeyStore || (await requestMasterKeyDownload($clientKeyStore.decryptKey))) {
|
||||
if (
|
||||
$masterKeyStore ||
|
||||
(await requestMasterKeyDownload($clientKeyStore.decryptKey, $clientKeyStore.verifyKey))
|
||||
) {
|
||||
await goto(data.redirectPath);
|
||||
} else {
|
||||
await redirect("/client/pending");
|
||||
|
||||
@@ -16,11 +16,13 @@
|
||||
if ($masterKeyStore) {
|
||||
goto(data.redirectPath);
|
||||
} else if ($clientKeyStore) {
|
||||
requestMasterKeyDownload($clientKeyStore.decryptKey).then(async (ok) => {
|
||||
requestMasterKeyDownload($clientKeyStore.decryptKey, $clientKeyStore.verifyKey).then(
|
||||
async (ok) => {
|
||||
if (ok) {
|
||||
return await goto(data.redirectPath);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { callAPI } from "$lib/hooks";
|
||||
import { storeClientKey } from "$lib/indexedDB";
|
||||
import { encodeToBase64, signRequest } from "$lib/modules/crypto";
|
||||
import { encodeToBase64, signRequest, signMasterKeyWrapped } from "$lib/modules/crypto";
|
||||
import type { ClientKeys } from "$lib/stores";
|
||||
|
||||
export { requestTokenUpgrade } from "$lib/services/auth";
|
||||
@@ -53,6 +53,7 @@ export const requestInitialMasterKeyRegistration = async (
|
||||
body: await signRequest(
|
||||
{
|
||||
mek: encodeToBase64(masterKeyWrapped),
|
||||
mekSig: await signMasterKeyWrapped(1, masterKeyWrapped, signKey),
|
||||
},
|
||||
signKey,
|
||||
),
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { text } from "@sveltejs/kit";
|
||||
import { z } from "zod";
|
||||
import { authorize } from "$lib/server/modules/auth";
|
||||
import { parseSignedRequest } from "$lib/server/modules/crypto";
|
||||
import { registerNewActiveMek } from "$lib/server/services/mek";
|
||||
import type { RequestHandler } from "@sveltejs/kit";
|
||||
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
const { userId, clientId } = await authorize(cookies, "activeClient");
|
||||
const { meks } = await parseSignedRequest(
|
||||
clientId,
|
||||
await request.json(),
|
||||
z.object({
|
||||
meks: z.array(
|
||||
z.object({
|
||||
clientId: z.number().int().positive(),
|
||||
mek: z.string().base64().nonempty(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
await registerNewActiveMek(
|
||||
userId,
|
||||
clientId,
|
||||
meks.map(({ clientId, mek }) => ({
|
||||
clientId,
|
||||
encMek: mek,
|
||||
})),
|
||||
);
|
||||
return text("MEK registered", { headers: { "Content-Type": "text/plain" } });
|
||||
};
|
||||
@@ -11,14 +11,15 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
error(403, "Forbidden");
|
||||
}
|
||||
|
||||
const { mek } = await parseSignedRequest(
|
||||
const { mek, mekSig } = await parseSignedRequest(
|
||||
clientId,
|
||||
await request.json(),
|
||||
z.object({
|
||||
mek: z.string().base64().nonempty(),
|
||||
mekSig: z.string().base64().nonempty(),
|
||||
}),
|
||||
);
|
||||
|
||||
await registerInitialActiveMek(userId, clientId, mek);
|
||||
await registerInitialActiveMek(userId, clientId, mek, mekSig);
|
||||
return text("MEK registered", { headers: { "Content-Type": "text/plain" } });
|
||||
};
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { text } from "@sveltejs/kit";
|
||||
import { z } from "zod";
|
||||
import { authorize } from "$lib/server/modules/auth";
|
||||
import { parseSignedRequest } from "$lib/server/modules/crypto";
|
||||
import { shareMeksForNewClient } from "$lib/server/services/mek";
|
||||
import type { RequestHandler } from "./$types";
|
||||
|
||||
export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
const { userId, clientId } = await authorize(cookies, "activeClient");
|
||||
const { targetId, meks } = await parseSignedRequest(
|
||||
clientId,
|
||||
await request.json(),
|
||||
z.object({
|
||||
targetId: z.number().int().positive(),
|
||||
meks: z.array(
|
||||
z.object({
|
||||
version: z.number().int().positive(),
|
||||
mek: z.string().base64().nonempty(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
await shareMeksForNewClient(
|
||||
userId,
|
||||
targetId,
|
||||
meks.map(({ version, mek }) => ({ version, encMek: mek })),
|
||||
);
|
||||
return text("MEK shared", { headers: { "Content-Type": "text/plain" } });
|
||||
};
|
||||
Reference in New Issue
Block a user