/api/mek/register, /api/mek/share Endpoint 삭제 및 MEK 서명 매커니즘 구현

2025년 첫 커밋! Happy New Year~
This commit is contained in:
static
2025-01-01 05:24:13 +09:00
parent e8e4022bc2
commit 363f809d02
12 changed files with 112 additions and 259 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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