From 805d7df182e8acde734e71b596d8b6d1cf447d04 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 12 Jan 2025 20:26:48 +0900 Subject: [PATCH 1/4] =?UTF-8?q?/api/hsk/list,=20/api/hsk/register/initial?= =?UTF-8?q?=20Endpoint=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/server/db/error.ts | 3 ++ src/lib/server/db/file.ts | 21 ++++++++- src/lib/server/db/hsk.ts | 45 +++++++++++++++++++ src/lib/server/db/schema/file.ts | 9 +++- src/lib/server/db/schema/hsk.ts | 43 ++++++++++++++++++ src/lib/server/db/schema/index.ts | 1 + src/lib/server/schemas/file.ts | 2 + src/lib/server/schemas/hsk.ts | 19 ++++++++ src/lib/server/schemas/index.ts | 1 + src/lib/server/services/hsk.ts | 31 +++++++++++++ src/routes/api/file/upload/+server.ts | 16 ++++++- src/routes/api/hsk/list/+server.ts | 20 +++++++++ .../api/hsk/register/initial/+server.ts | 16 +++++++ 13 files changed, 223 insertions(+), 4 deletions(-) create mode 100644 src/lib/server/db/hsk.ts create mode 100644 src/lib/server/db/schema/hsk.ts create mode 100644 src/lib/server/schemas/hsk.ts create mode 100644 src/lib/server/services/hsk.ts create mode 100644 src/routes/api/hsk/list/+server.ts create mode 100644 src/routes/api/hsk/register/initial/+server.ts diff --git a/src/lib/server/db/error.ts b/src/lib/server/db/error.ts index beadb6f..547cc6c 100644 --- a/src/lib/server/db/error.ts +++ b/src/lib/server/db/error.ts @@ -8,6 +8,9 @@ type IntegrityErrorMessages = | "Directory not found" | "File not found" | "Invalid DEK version" + // HSK + | "HSK already registered" + | "Inactive HSK version" // MEK | "MEK already registered" | "Inactive MEK version" diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index edf0249..1ce230c 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -1,7 +1,7 @@ import { and, eq, isNull } from "drizzle-orm"; import db from "./drizzle"; import { IntegrityError } from "./error"; -import { directory, directoryLog, file, fileLog, mek } from "./schema"; +import { directory, directoryLog, file, fileLog, hsk, mek } from "./schema"; type DirectoryId = "root" | number; @@ -22,6 +22,8 @@ export interface NewFileParams { mekVersion: number; encDek: string; dekVersion: Date; + hskVersion: number | null; + contentHmac: string | null; contentType: string; encContentIv: string; encName: string; @@ -152,6 +154,10 @@ export const unregisterDirectory = async (userId: number, directoryId: number) = }; export const registerFile = async (params: NewFileParams) => { + if ((params.hskVersion && !params.contentHmac) || (!params.hskVersion && params.contentHmac)) { + throw new Error("Invalid arguments"); + } + await db.transaction( async (tx) => { const meks = await tx @@ -163,6 +169,17 @@ export const registerFile = async (params: NewFileParams) => { throw new IntegrityError("Inactive MEK version"); } + if (params.hskVersion) { + const hsks = await tx + .select({ version: hsk.version }) + .from(hsk) + .where(and(eq(hsk.userId, params.userId), eq(hsk.state, "active"))) + .limit(1); + if (hsks[0]?.version !== params.hskVersion) { + throw new IntegrityError("Inactive HSK version"); + } + } + const newFiles = await tx .insert(file) .values({ @@ -170,6 +187,8 @@ export const registerFile = async (params: NewFileParams) => { parentId: params.parentId === "root" ? null : params.parentId, userId: params.userId, mekVersion: params.mekVersion, + hskVersion: params.hskVersion, + contentHmac: params.contentHmac, contentType: params.contentType, encDek: params.encDek, dekVersion: params.dekVersion, diff --git a/src/lib/server/db/hsk.ts b/src/lib/server/db/hsk.ts new file mode 100644 index 0000000..809ed7b --- /dev/null +++ b/src/lib/server/db/hsk.ts @@ -0,0 +1,45 @@ +import { SqliteError } from "better-sqlite3"; +import { and, eq } from "drizzle-orm"; +import db from "./drizzle"; +import { IntegrityError } from "./error"; +import { hsk, hskLog } from "./schema"; + +export const registerInitialHsk = async ( + userId: number, + createdBy: number, + mekVersion: number, + encHsk: string, +) => { + await db.transaction( + async (tx) => { + try { + await tx.insert(hsk).values({ + userId, + version: 1, + state: "active", + mekVersion, + encHsk, + }); + await tx.insert(hskLog).values({ + userId, + hskVersion: 1, + timestamp: new Date(), + action: "create", + actionBy: createdBy, + }); + } catch (e) { + if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_PRIMARYKEY") { + throw new IntegrityError("HSK already registered"); + } + } + }, + { behavior: "exclusive" }, + ); +}; + +export const getAllValidHsks = async (userId: number) => { + return await db + .select() + .from(hsk) + .where(and(eq(hsk.userId, userId), eq(hsk.state, "active"))); +}; diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts index 0bae91a..7ac0b77 100644 --- a/src/lib/server/db/schema/file.ts +++ b/src/lib/server/db/schema/file.ts @@ -1,4 +1,5 @@ import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core"; +import { hsk } from "./hsk"; import { mek } from "./mek"; import { user } from "./user"; @@ -55,15 +56,21 @@ export const file = sqliteTable( mekVersion: integer("master_encryption_key_version").notNull(), encDek: text("encrypted_data_encryption_key").notNull().unique(), // Base64 dekVersion: integer("data_encryption_key_version", { mode: "timestamp_ms" }).notNull(), + hskVersion: integer("hmac_secret_key_version"), + contentHmac: text("content_hmac"), // Base64 contentType: text("content_type").notNull(), encContentIv: text("encrypted_content_iv").notNull(), // Base64 encName: ciphertext("encrypted_name").notNull(), }, (t) => ({ - ref: foreignKey({ + ref1: foreignKey({ columns: [t.userId, t.mekVersion], foreignColumns: [mek.userId, mek.version], }), + ref2: foreignKey({ + columns: [t.userId, t.hskVersion], + foreignColumns: [hsk.userId, hsk.version], + }), }), ); diff --git a/src/lib/server/db/schema/hsk.ts b/src/lib/server/db/schema/hsk.ts new file mode 100644 index 0000000..b78c512 --- /dev/null +++ b/src/lib/server/db/schema/hsk.ts @@ -0,0 +1,43 @@ +import { sqliteTable, text, integer, primaryKey, foreignKey } from "drizzle-orm/sqlite-core"; +import { mek } from "./mek"; +import { user } from "./user"; + +export const hsk = sqliteTable( + "hmac_secret_key", + { + userId: integer("user_id") + .notNull() + .references(() => user.id), + version: integer("version").notNull(), + state: text("state", { enum: ["active"] }).notNull(), + mekVersion: integer("master_encryption_key_version").notNull(), + encHsk: text("encrypted_key").notNull().unique(), // Base64 + }, + (t) => ({ + pk: primaryKey({ columns: [t.userId, t.version] }), + ref: foreignKey({ + columns: [t.userId, t.mekVersion], + foreignColumns: [mek.userId, mek.version], + }), + }), +); + +export const hskLog = sqliteTable( + "hmac_secret_key_log", + { + id: integer("id").primaryKey({ autoIncrement: true }), + userId: integer("user_id") + .notNull() + .references(() => user.id), + hskVersion: integer("hmac_secret_key_version").notNull(), + timestamp: integer("timestamp", { mode: "timestamp_ms" }).notNull(), + action: text("action", { enum: ["create"] }).notNull(), + actionBy: integer("action_by").references(() => user.id), + }, + (t) => ({ + ref: foreignKey({ + columns: [t.userId, t.hskVersion], + foreignColumns: [hsk.userId, hsk.version], + }), + }), +); diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 41fd4fe..40cb9be 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -1,5 +1,6 @@ export * from "./client"; export * from "./file"; +export * from "./hsk"; export * from "./mek"; export * from "./session"; export * from "./user"; diff --git a/src/lib/server/schemas/file.ts b/src/lib/server/schemas/file.ts index 0592835..958c4d7 100644 --- a/src/lib/server/schemas/file.ts +++ b/src/lib/server/schemas/file.ts @@ -27,6 +27,8 @@ export const fileUploadRequest = z.object({ mekVersion: z.number().int().positive(), dek: z.string().base64().nonempty(), dekVersion: z.string().datetime(), + hskVersion: z.number().int().positive(), + contentHmac: z.string().base64().nonempty(), contentType: z .string() .nonempty() diff --git a/src/lib/server/schemas/hsk.ts b/src/lib/server/schemas/hsk.ts new file mode 100644 index 0000000..bcea3cd --- /dev/null +++ b/src/lib/server/schemas/hsk.ts @@ -0,0 +1,19 @@ +import { z } from "zod"; + +export const hmacSecretListResponse = z.object({ + hsks: z.array( + z.object({ + version: z.number().int().positive(), + state: z.enum(["active"]), + mekVersion: z.number().int().positive(), + hsk: z.string().base64().nonempty(), + }), + ), +}); +export type HmacSecretListResponse = z.infer; + +export const initialHmacSecretRegisterRequest = z.object({ + mekVersion: z.number().int().positive(), + hsk: z.string().base64().nonempty(), +}); +export type InitialHmacSecretRegisterRequest = z.infer; diff --git a/src/lib/server/schemas/index.ts b/src/lib/server/schemas/index.ts index cd2a366..615e2bc 100644 --- a/src/lib/server/schemas/index.ts +++ b/src/lib/server/schemas/index.ts @@ -2,4 +2,5 @@ export * from "./auth"; export * from "./client"; export * from "./directory"; export * from "./file"; +export * from "./hsk"; export * from "./mek"; diff --git a/src/lib/server/services/hsk.ts b/src/lib/server/services/hsk.ts new file mode 100644 index 0000000..c381c51 --- /dev/null +++ b/src/lib/server/services/hsk.ts @@ -0,0 +1,31 @@ +import { error } from "@sveltejs/kit"; +import { IntegrityError } from "$lib/server/db/error"; +import { registerInitialHsk, getAllValidHsks } from "$lib/server/db/hsk"; + +export const getHskList = async (userId: number) => { + const hsks = await getAllValidHsks(userId); + return { + encHsks: hsks.map(({ version, state, mekVersion, encHsk }) => ({ + version, + state, + mekVersion, + encHsk, + })), + }; +}; + +export const registerInitialActiveHsk = async ( + userId: number, + createdBy: number, + mekVersion: number, + encHsk: string, +) => { + try { + await registerInitialHsk(userId, createdBy, mekVersion, encHsk); + } catch (e) { + if (e instanceof IntegrityError && e.message === "HSK already registered") { + error(409, "Initial HSK already registered"); + } + throw e; + } +}; diff --git a/src/routes/api/file/upload/+server.ts b/src/routes/api/file/upload/+server.ts index ac1ac51..1bc9ce2 100644 --- a/src/routes/api/file/upload/+server.ts +++ b/src/routes/api/file/upload/+server.ts @@ -16,8 +16,18 @@ export const POST: RequestHandler = async ({ locals, request }) => { const zodRes = fileUploadRequest.safeParse(JSON.parse(metadata)); if (!zodRes.success) error(400, "Invalid request body"); - const { parentId, mekVersion, dek, dekVersion, contentType, contentIv, name, nameIv } = - zodRes.data; + const { + parentId, + mekVersion, + dek, + dekVersion, + hskVersion, + contentHmac, + contentType, + contentIv, + name, + nameIv, + } = zodRes.data; await uploadFile( { @@ -26,6 +36,8 @@ export const POST: RequestHandler = async ({ locals, request }) => { mekVersion, encDek: dek, dekVersion: new Date(dekVersion), + hskVersion, + contentHmac, contentType, encContentIv: contentIv, encName: name, diff --git a/src/routes/api/hsk/list/+server.ts b/src/routes/api/hsk/list/+server.ts new file mode 100644 index 0000000..50957a3 --- /dev/null +++ b/src/routes/api/hsk/list/+server.ts @@ -0,0 +1,20 @@ +import { json } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { hmacSecretListResponse, type HmacSecretListResponse } from "$lib/server/schemas"; +import { getHskList } from "$lib/server/services/hsk"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ locals }) => { + const { userId } = await authorize(locals, "activeClient"); + const { encHsks } = await getHskList(userId); + return json( + hmacSecretListResponse.parse({ + hsks: encHsks.map(({ version, state, mekVersion, encHsk }) => ({ + version, + state, + mekVersion, + hsk: encHsk, + })), + } satisfies HmacSecretListResponse), + ); +}; diff --git a/src/routes/api/hsk/register/initial/+server.ts b/src/routes/api/hsk/register/initial/+server.ts new file mode 100644 index 0000000..8b32952 --- /dev/null +++ b/src/routes/api/hsk/register/initial/+server.ts @@ -0,0 +1,16 @@ +import { error, text } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { initialHmacSecretRegisterRequest } from "$lib/server/schemas"; +import { registerInitialActiveHsk } from "$lib/server/services/hsk"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId, clientId } = await authorize(locals, "activeClient"); + + const zodRes = initialHmacSecretRegisterRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { mekVersion, hsk } = zodRes.data; + + await registerInitialActiveHsk(userId, clientId, mekVersion, hsk); + return text("HSK registered", { headers: { "Content-Type": "text/plain" } }); +}; From 59c8523e25d10f1d0202ef60f72340e3a24c4e67 Mon Sep 17 00:00:00 2001 From: static Date: Sun, 12 Jan 2025 21:52:41 +0900 Subject: [PATCH 2/4] =?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 @@ + + +
+
+

이미 업로드된 파일이에요.

+

그래도 업로드할까요?

+
+
+ + +
+
+
diff --git a/src/routes/(main)/directory/[[id]]/service.ts b/src/routes/(main)/directory/[[id]]/service.ts index b237ae5..32575fb 100644 --- a/src/routes/(main)/directory/[[id]]/service.ts +++ b/src/routes/(main)/directory/[[id]]/service.ts @@ -15,6 +15,8 @@ import type { FileRenameRequest, FileUploadRequest, HmacSecretListResponse, + DuplicateFileScanRequest, + DuplicateFileScanResponse, } from "$lib/server/schemas"; import { hmacSecretStore, type MasterKey, type HmacSecret } from "$lib/stores"; @@ -63,17 +65,33 @@ export const requestDirectoryCreation = async ( }); }; +export const requestDuplicateFileScan = async (file: File, hmacSecret: HmacSecret) => { + const fileBuffer = await file.arrayBuffer(); + const fileSigned = encodeToBase64(await signMessageHmac(fileBuffer, hmacSecret.secret)); + const res = await callPostApi("/api/file/scanDuplicates", { + hskVersion: hmacSecret.version, + contentHmac: fileSigned, + }); + if (!res.ok) return null; + + const { files }: DuplicateFileScanResponse = await res.json(); + return { + fileBuffer, + fileSigned, + isDuplicate: files.length > 0, + }; +}; + export const requestFileUpload = async ( file: File, + fileBuffer: ArrayBuffer, + fileSigned: string, parentId: "root" | number, masterKey: MasterKey, hmacSecret: HmacSecret, ) => { const { dataKey, dataKeyVersion } = await generateDataKey(); 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(); @@ -85,7 +103,7 @@ export const requestFileUpload = async ( dek: await wrapDataKey(dataKey, masterKey.key), dekVersion: dataKeyVersion.toISOString(), hskVersion: hmacSecret.version, - contentHmac: encodeToBase64(fileSigned), + contentHmac: fileSigned, contentType: file.type, contentIv: fileEncrypted.iv, name: nameEncrypted.ciphertext, diff --git a/src/routes/api/file/scanDuplicates/+server.ts b/src/routes/api/file/scanDuplicates/+server.ts new file mode 100644 index 0000000..fb41b43 --- /dev/null +++ b/src/routes/api/file/scanDuplicates/+server.ts @@ -0,0 +1,20 @@ +import { error, json } from "@sveltejs/kit"; +import { authorize } from "$lib/server/modules/auth"; +import { + duplicateFileScanRequest, + duplicateFileScanResponse, + type DuplicateFileScanResponse, +} from "$lib/server/schemas"; +import { scanDuplicateFiles } from "$lib/server/services/file"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ locals, request }) => { + const { userId } = await authorize(locals, "activeClient"); + + const zodRes = duplicateFileScanRequest.safeParse(await request.json()); + if (!zodRes.success) error(400, "Invalid request body"); + const { hskVersion, contentHmac } = zodRes.data; + + const { files } = await scanDuplicateFiles(userId, hskVersion, contentHmac); + return json(duplicateFileScanResponse.parse({ files } satisfies DuplicateFileScanResponse)); +}; From e887fcf1372bf2315a465a9879f4ee5e3e5b8933 Mon Sep 17 00:00:00 2001 From: static Date: Mon, 13 Jan 2025 00:48:08 +0900 Subject: [PATCH 4/4] =?UTF-8?q?DB=20=EB=A7=88=EC=9D=B4=EA=B7=B8=EB=A0=88?= =?UTF-8?q?=EC=9D=B4=EC=85=98=20=EC=8A=A4=ED=81=AC=EB=A6=BD=ED=8A=B8=20?= =?UTF-8?q?=EC=9E=AC=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ..._bullseye.sql => 0000_lush_black_bolt.sql} | 64 ++- drizzle/meta/0000_snapshot.json | 473 ++++++++++++++++-- drizzle/meta/_journal.json | 4 +- 3 files changed, 486 insertions(+), 55 deletions(-) rename drizzle/{0000_spooky_lady_bullseye.sql => 0000_lush_black_bolt.sql} (68%) diff --git a/drizzle/0000_spooky_lady_bullseye.sql b/drizzle/0000_lush_black_bolt.sql similarity index 68% rename from drizzle/0000_spooky_lady_bullseye.sql rename to drizzle/0000_lush_black_bolt.sql index d9b520c..4012e91 100644 --- a/drizzle/0000_spooky_lady_bullseye.sql +++ b/drizzle/0000_lush_black_bolt.sql @@ -27,7 +27,6 @@ CREATE TABLE `user_client_challenge` ( --> statement-breakpoint CREATE TABLE `directory` ( `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `created_at` integer NOT NULL, `parent_id` integer, `user_id` integer NOT NULL, `master_encryption_key_version` integer NOT NULL, @@ -39,23 +38,66 @@ CREATE TABLE `directory` ( FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint +CREATE TABLE `directory_log` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `directory_id` integer NOT NULL, + `timestamp` integer NOT NULL, + `action` text NOT NULL, + `new_name` text, + FOREIGN KEY (`directory_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint CREATE TABLE `file` ( `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, - `path` text NOT NULL, `parent_id` integer, - `created_at` integer NOT NULL, `user_id` integer NOT NULL, + `path` text NOT NULL, `master_encryption_key_version` integer NOT NULL, `encrypted_data_encryption_key` text NOT NULL, `data_encryption_key_version` integer NOT NULL, + `hmac_secret_key_version` integer, + `content_hmac` text, `content_type` text NOT NULL, `encrypted_content_iv` text NOT NULL, `encrypted_name` text NOT NULL, FOREIGN KEY (`parent_id`) REFERENCES `directory`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`,`hmac_secret_key_version`) REFERENCES `hmac_secret_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `file_log` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `file_id` integer NOT NULL, + `timestamp` integer NOT NULL, + `action` text NOT NULL, + `new_name` text, + FOREIGN KEY (`file_id`) REFERENCES `file`(`id`) ON UPDATE no action ON DELETE cascade +); +--> statement-breakpoint +CREATE TABLE `hmac_secret_key` ( + `user_id` integer NOT NULL, + `version` integer NOT NULL, + `state` text NOT NULL, + `master_encryption_key_version` integer NOT NULL, + `encrypted_key` text NOT NULL, + PRIMARY KEY(`user_id`, `version`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint +CREATE TABLE `hmac_secret_key_log` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer NOT NULL, + `hmac_secret_key_version` integer NOT NULL, + `timestamp` integer NOT NULL, + `action` text NOT NULL, + `action_by` integer, + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`action_by`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`,`hmac_secret_key_version`) REFERENCES `hmac_secret_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint CREATE TABLE `client_master_encryption_key` ( `user_id` integer NOT NULL, `client_id` integer NOT NULL, @@ -71,13 +113,22 @@ CREATE TABLE `client_master_encryption_key` ( CREATE TABLE `master_encryption_key` ( `user_id` integer NOT NULL, `version` integer NOT NULL, - `created_by` integer NOT NULL, - `created_at` integer NOT NULL, `state` text NOT NULL, `retired_at` integer, PRIMARY KEY(`user_id`, `version`), + FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `master_encryption_key_log` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer NOT NULL, + `master_encryption_key_version` integer NOT NULL, + `timestamp` integer NOT NULL, + `action` text NOT NULL, + `action_by` integer, FOREIGN KEY (`user_id`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE no action, - FOREIGN KEY (`created_by`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action + FOREIGN KEY (`action_by`) REFERENCES `client`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`user_id`,`master_encryption_key_version`) REFERENCES `master_encryption_key`(`user_id`,`version`) ON UPDATE no action ON DELETE no action ); --> statement-breakpoint CREATE TABLE `session` ( @@ -116,6 +167,7 @@ CREATE UNIQUE INDEX `user_client_challenge_answer_unique` ON `user_client_challe CREATE UNIQUE INDEX `directory_encrypted_data_encryption_key_unique` ON `directory` (`encrypted_data_encryption_key`);--> statement-breakpoint CREATE UNIQUE INDEX `file_path_unique` ON `file` (`path`);--> statement-breakpoint CREATE UNIQUE INDEX `file_encrypted_data_encryption_key_unique` ON `file` (`encrypted_data_encryption_key`);--> statement-breakpoint +CREATE UNIQUE INDEX `hmac_secret_key_encrypted_key_unique` ON `hmac_secret_key` (`encrypted_key`);--> statement-breakpoint CREATE UNIQUE INDEX `session_user_id_client_id_unique` ON `session` (`user_id`,`client_id`);--> statement-breakpoint CREATE UNIQUE INDEX `session_upgrade_challenge_session_id_unique` ON `session_upgrade_challenge` (`session_id`);--> statement-breakpoint CREATE UNIQUE INDEX `session_upgrade_challenge_answer_unique` ON `session_upgrade_challenge` (`answer`);--> statement-breakpoint diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index 57c4a6a..4905c11 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,7 +1,7 @@ { "version": "6", "dialect": "sqlite", - "id": "c518e1b4-38f8-4c8e-bdc9-64152ab456d8", + "id": "f2fbe45c-1f1d-4dd8-92ab-dd057c0e668b", "prevId": "00000000-0000-0000-0000-000000000000", "tables": { "client": { @@ -234,13 +234,6 @@ "notNull": true, "autoincrement": true }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "parent_id": { "name": "parent_id", "type": "integer", @@ -339,6 +332,64 @@ "compositePrimaryKeys": {}, "uniqueConstraints": {} }, + "directory_log": { + "name": "directory_log", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "directory_id": { + "name": "directory_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "new_name": { + "name": "new_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "directory_log_directory_id_directory_id_fk": { + "name": "directory_log_directory_id_directory_id_fk", + "tableFrom": "directory_log", + "tableTo": "directory", + "columnsFrom": [ + "directory_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, "file": { "name": "file", "columns": { @@ -349,13 +400,6 @@ "notNull": true, "autoincrement": true }, - "path": { - "name": "path", - "type": "text", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "parent_id": { "name": "parent_id", "type": "integer", @@ -363,16 +407,16 @@ "notNull": false, "autoincrement": false }, - "created_at": { - "name": "created_at", + "user_id": { + "name": "user_id", "type": "integer", "primaryKey": false, "notNull": true, "autoincrement": false }, - "user_id": { - "name": "user_id", - "type": "integer", + "path": { + "name": "path", + "type": "text", "primaryKey": false, "notNull": true, "autoincrement": false @@ -398,6 +442,20 @@ "notNull": true, "autoincrement": false }, + "hmac_secret_key_version": { + "name": "hmac_secret_key_version", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content_hmac": { + "name": "content_hmac", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, "content_type": { "name": "content_type", "type": "text", @@ -477,6 +535,261 @@ ], "onDelete": "no action", "onUpdate": "no action" + }, + "file_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk": { + "name": "file_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk", + "tableFrom": "file", + "tableTo": "hmac_secret_key", + "columnsFrom": [ + "user_id", + "hmac_secret_key_version" + ], + "columnsTo": [ + "user_id", + "version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "file_log": { + "name": "file_log", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "file_id": { + "name": "file_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "new_name": { + "name": "new_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "file_log_file_id_file_id_fk": { + "name": "file_log_file_id_file_id_fk", + "tableFrom": "file_log", + "tableTo": "file", + "columnsFrom": [ + "file_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "hmac_secret_key": { + "name": "hmac_secret_key", + "columns": { + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "master_encryption_key_version": { + "name": "master_encryption_key_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "encrypted_key": { + "name": "encrypted_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "hmac_secret_key_encrypted_key_unique": { + "name": "hmac_secret_key_encrypted_key_unique", + "columns": [ + "encrypted_key" + ], + "isUnique": true + } + }, + "foreignKeys": { + "hmac_secret_key_user_id_user_id_fk": { + "name": "hmac_secret_key_user_id_user_id_fk", + "tableFrom": "hmac_secret_key", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hmac_secret_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { + "name": "hmac_secret_key_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", + "tableFrom": "hmac_secret_key", + "tableTo": "master_encryption_key", + "columnsFrom": [ + "user_id", + "master_encryption_key_version" + ], + "columnsTo": [ + "user_id", + "version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "hmac_secret_key_user_id_version_pk": { + "columns": [ + "user_id", + "version" + ], + "name": "hmac_secret_key_user_id_version_pk" + } + }, + "uniqueConstraints": {} + }, + "hmac_secret_key_log": { + "name": "hmac_secret_key_log", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "hmac_secret_key_version": { + "name": "hmac_secret_key_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action_by": { + "name": "action_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "hmac_secret_key_log_user_id_user_id_fk": { + "name": "hmac_secret_key_log_user_id_user_id_fk", + "tableFrom": "hmac_secret_key_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hmac_secret_key_log_action_by_user_id_fk": { + "name": "hmac_secret_key_log_action_by_user_id_fk", + "tableFrom": "hmac_secret_key_log", + "tableTo": "user", + "columnsFrom": [ + "action_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "hmac_secret_key_log_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk": { + "name": "hmac_secret_key_log_user_id_hmac_secret_key_version_hmac_secret_key_user_id_version_fk", + "tableFrom": "hmac_secret_key_log", + "tableTo": "hmac_secret_key", + "columnsFrom": [ + "user_id", + "hmac_secret_key_version" + ], + "columnsTo": [ + "user_id", + "version" + ], + "onDelete": "no action", + "onUpdate": "no action" } }, "compositePrimaryKeys": {}, @@ -594,20 +907,6 @@ "notNull": true, "autoincrement": false }, - "created_by": { - "name": "created_by", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, - "created_at": { - "name": "created_at", - "type": "integer", - "primaryKey": false, - "notNull": true, - "autoincrement": false - }, "state": { "name": "state", "type": "text", @@ -637,19 +936,6 @@ ], "onDelete": "no action", "onUpdate": "no action" - }, - "master_encryption_key_created_by_client_id_fk": { - "name": "master_encryption_key_created_by_client_id_fk", - "tableFrom": "master_encryption_key", - "tableTo": "client", - "columnsFrom": [ - "created_by" - ], - "columnsTo": [ - "id" - ], - "onDelete": "no action", - "onUpdate": "no action" } }, "compositePrimaryKeys": { @@ -663,6 +949,99 @@ }, "uniqueConstraints": {} }, + "master_encryption_key_log": { + "name": "master_encryption_key_log", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "master_encryption_key_version": { + "name": "master_encryption_key_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "timestamp": { + "name": "timestamp", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "action_by": { + "name": "action_by", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "master_encryption_key_log_user_id_user_id_fk": { + "name": "master_encryption_key_log_user_id_user_id_fk", + "tableFrom": "master_encryption_key_log", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "master_encryption_key_log_action_by_client_id_fk": { + "name": "master_encryption_key_log_action_by_client_id_fk", + "tableFrom": "master_encryption_key_log", + "tableTo": "client", + "columnsFrom": [ + "action_by" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "master_encryption_key_log_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk": { + "name": "master_encryption_key_log_user_id_master_encryption_key_version_master_encryption_key_user_id_version_fk", + "tableFrom": "master_encryption_key_log", + "tableTo": "master_encryption_key", + "columnsFrom": [ + "user_id", + "master_encryption_key_version" + ], + "columnsTo": [ + "user_id", + "version" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, "session": { "name": "session", "columns": { diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 62c9f38..723ede5 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "6", - "when": 1736637983139, - "tag": "0000_spooky_lady_bullseye", + "when": 1736696839327, + "tag": "0000_lush_black_bolt", "breakpoints": true } ]