diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts new file mode 100644 index 0000000..a693f48 --- /dev/null +++ b/src/lib/server/db/file.ts @@ -0,0 +1,73 @@ +import { and, eq, isNull } from "drizzle-orm"; +import db from "./drizzle"; +import { directory, file, mek } from "./schema"; + +type DirectroyId = "root" | number; + +export interface NewDirectroyParams { + userId: number; + parentId: DirectroyId; + mekVersion: number; + encDek: string; + encDekIv: string; + encName: string; + encNameIv: string; +} + +export const registerNewDirectory = async (params: NewDirectroyParams) => { + return await db.transaction(async (tx) => { + const meks = await tx + .select() + .from(mek) + .where(and(eq(mek.userId, params.userId), eq(mek.state, "active"))); + if (meks[0]?.version !== params.mekVersion) { + throw new Error("Invalid MEK version"); + } + + const now = new Date(); + await tx.insert(directory).values({ + createdAt: now, + parentId: params.parentId === "root" ? null : params.parentId, + userId: params.userId, + mekVersion: params.mekVersion, + encDek: { ciphertext: params.encDek, iv: params.encDekIv }, + encryptedAt: now, + encName: { ciphertext: params.encName, iv: params.encNameIv }, + }); + }); +}; + +export const getAllDirectoriesByParent = async (userId: number, directoryId: DirectroyId) => { + return await db + .select() + .from(directory) + .where( + and( + eq(directory.userId, userId), + directoryId === "root" ? isNull(directory.parentId) : eq(directory.parentId, directoryId), + ), + ) + .execute(); +}; + +export const getDirectory = async (userId: number, directoryId: number) => { + const res = await db + .select() + .from(directory) + .where(and(eq(directory.userId, userId), eq(directory.id, directoryId))) + .execute(); + return res[0] ?? null; +}; + +export const getAllFilesByParent = async (userId: number, parentId: DirectroyId) => { + return await db + .select() + .from(file) + .where( + and( + eq(file.userId, userId), + parentId === "root" ? isNull(file.parentId) : eq(file.parentId, parentId), + ), + ) + .execute(); +}; diff --git a/src/lib/server/db/mek.ts b/src/lib/server/db/mek.ts index 94e6553..7215ce0 100644 --- a/src/lib/server/db/mek.ts +++ b/src/lib/server/db/mek.ts @@ -35,6 +35,15 @@ export const getInitialMek = async (userId: number) => { return meks[0] ?? null; }; +export const getActiveMekVersion = async (userId: number) => { + const meks = await db + .select({ version: mek.version }) + .from(mek) + .where(and(eq(mek.userId, userId), eq(mek.state, "active"))) + .execute(); + return meks[0]?.version ?? null; +}; + export const getAllValidClientMeks = async (userId: number, clientId: number) => { return await db .select() diff --git a/src/lib/server/db/schema/file.ts b/src/lib/server/db/schema/file.ts new file mode 100644 index 0000000..f56e294 --- /dev/null +++ b/src/lib/server/db/schema/file.ts @@ -0,0 +1,58 @@ +import { sqliteTable, text, integer, foreignKey } from "drizzle-orm/sqlite-core"; +import { mek } from "./mek"; +import { user } from "./user"; + +const ciphertext = (name: string) => + text(name, { mode: "json" }).$type<{ + ciphertext: string; + iv: string; + }>(); + +export const directory = sqliteTable( + "directory", + { + id: integer("id").primaryKey({ autoIncrement: true }), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + parentId: integer("parent_id"), + userId: integer("user_id") + .notNull() + .references(() => user.id), + mekVersion: integer("master_encryption_key_version").notNull(), + encDek: ciphertext("encrypted_data_encryption_key").notNull().unique(), + encryptedAt: integer("encrypted_at", { mode: "timestamp_ms" }).notNull(), + encName: ciphertext("encrypted_name").notNull(), + }, + (t) => ({ + ref1: foreignKey({ + columns: [t.parentId], + foreignColumns: [t.id], + }), + ref2: foreignKey({ + columns: [t.userId, t.mekVersion], + foreignColumns: [mek.userId, mek.version], + }), + }), +); + +export const file = sqliteTable( + "file", + { + id: integer("id").primaryKey({ autoIncrement: true }), + path: text("path").notNull().unique(), + parentId: integer("parent_id").references(() => directory.id), + createdAt: integer("created_at", { mode: "timestamp_ms" }).notNull(), + userId: integer("user_id") + .notNull() + .references(() => user.id), + mekVersion: integer("master_encryption_key_version").notNull(), + encDek: ciphertext("encrypted_data_encryption_key").notNull().unique(), + encryptedAt: integer("encrypted_at", { mode: "timestamp_ms" }).notNull(), + encName: ciphertext("encrypted_name").notNull(), + }, + (t) => ({ + ref: foreignKey({ + columns: [t.userId, t.mekVersion], + foreignColumns: [mek.userId, mek.version], + }), + }), +); diff --git a/src/lib/server/db/schema/index.ts b/src/lib/server/db/schema/index.ts index 0cae4a4..4344b00 100644 --- a/src/lib/server/db/schema/index.ts +++ b/src/lib/server/db/schema/index.ts @@ -1,4 +1,5 @@ export * from "./client"; +export * from "./file"; export * from "./mek"; export * from "./token"; export * from "./user"; diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts new file mode 100644 index 0000000..8cabf7b --- /dev/null +++ b/src/lib/server/services/file.ts @@ -0,0 +1,41 @@ +import { error } from "@sveltejs/kit"; +import { + getAllDirectoriesByParent, + registerNewDirectory, + getDirectory, + getAllFilesByParent, + type NewDirectroyParams, +} from "$lib/server/db/file"; +import { getActiveMekVersion } from "$lib/server/db/mek"; + +export const getDirectroyInformation = async (userId: number, directroyId: "root" | number) => { + const directory = directroyId !== "root" ? await getDirectory(userId, directroyId) : undefined; + if (directory === null) { + error(404, "Invalid directory id"); + } + + const directories = await getAllDirectoriesByParent(userId, directroyId); + const files = await getAllFilesByParent(userId, directroyId); + + return { + metadata: directory && { + createdAt: directory.createdAt, + mekVersion: directory.mekVersion, + encDek: directory.encDek, + encName: directory.encName, + }, + directories: directories.map(({ id }) => id), + files: files.map(({ id }) => id), + }; +}; + +export const createDirectory = async (params: NewDirectroyParams) => { + const activeMekVersion = await getActiveMekVersion(params.userId); + if (activeMekVersion === null) { + error(500, "Invalid MEK version"); + } else if (activeMekVersion !== params.mekVersion) { + error(400, "Invalid MEK version"); + } + + await registerNewDirectory(params); +}; diff --git a/src/routes/api/directory/[id]/+server.ts b/src/routes/api/directory/[id]/+server.ts new file mode 100644 index 0000000..6decc3b --- /dev/null +++ b/src/routes/api/directory/[id]/+server.ts @@ -0,0 +1,31 @@ +import { error, json } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { getDirectroyInformation } from "$lib/server/services/file"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = async ({ cookies, params }) => { + const { userId } = await authorize(cookies, "activeClient"); + + const zodRes = z + .object({ + id: z.union([z.enum(["root"]), z.coerce.number().int().positive()]), + }) + .safeParse(params); + if (!zodRes.success) error(400, "Invalid path parameters"); + const { id } = zodRes.data; + + const { metadata, directories, files } = await getDirectroyInformation(userId, id); + return json({ + metadata: metadata && { + createdAt: metadata.createdAt, + mekVersion: metadata.mekVersion, + dek: metadata.encDek.ciphertext, + dekIv: metadata.encDek.iv, + name: metadata.encName.ciphertext, + nameIv: metadata.encName.iv, + }, + subDirectories: directories, + files, + }); +}; diff --git a/src/routes/api/directory/create/+server.ts b/src/routes/api/directory/create/+server.ts new file mode 100644 index 0000000..e51ec80 --- /dev/null +++ b/src/routes/api/directory/create/+server.ts @@ -0,0 +1,33 @@ +import { text } from "@sveltejs/kit"; +import { z } from "zod"; +import { authorize } from "$lib/server/modules/auth"; +import { parseSignedRequest } from "$lib/server/modules/crypto"; +import { createDirectory } from "$lib/server/services/file"; +import type { RequestHandler } from "./$types"; + +export const POST: RequestHandler = async ({ request, cookies }) => { + const { userId, clientId } = await authorize(cookies, "activeClient"); + const { parentId, mekVersion, dek, dekIv, name, nameIv } = await parseSignedRequest( + clientId, + await request.json(), + z.object({ + parentId: z.union([z.enum(["root"]), z.number().int().positive()]), + mekVersion: z.number().int().positive(), + dek: z.string().base64().nonempty(), + dekIv: z.string().base64().nonempty(), + name: z.string().base64().nonempty(), + nameIv: z.string().base64().nonempty(), + }), + ); + + await createDirectory({ + userId, + parentId, + mekVersion, + encDek: dek, + encDekIv: dekIv, + encName: name, + encNameIv: nameIv, + }); + return text("Directory created", { headers: { "Content-Type": "text/plain" } }); +};