diff --git a/src/lib/components/ui/file-card/file-card.svelte b/src/lib/components/ui/file-card/file-card.svelte new file mode 100644 index 0000000..53c02e3 --- /dev/null +++ b/src/lib/components/ui/file-card/file-card.svelte @@ -0,0 +1,154 @@ + + + + + + + {#snippet child({ props })} +
+ {file.name} +
+ {/snippet} +
+ +

{file.name}

+
+
+
+ + {filesize(file.size)} + + + + +
diff --git a/src/lib/components/ui/file-card/index.ts b/src/lib/components/ui/file-card/index.ts new file mode 100644 index 0000000..f578e2c --- /dev/null +++ b/src/lib/components/ui/file-card/index.ts @@ -0,0 +1,3 @@ +import FileCard from "./file-card.svelte"; + +export { FileCard }; diff --git a/src/lib/server/trpc/router/blob_management.ts b/src/lib/server/trpc/router/blob_management.ts new file mode 100644 index 0000000..17909b0 --- /dev/null +++ b/src/lib/server/trpc/router/blob_management.ts @@ -0,0 +1,114 @@ +import { DB } from "$lib/server/db"; +import { authMiddleware } from "../middleware"; +import { trpcInstance } from "./init"; + +import { + publicKeyTable, + encryptedBlobTable, + symmetricKeyTable, +} from "$lib/drizzle"; +import { and, eq } from "drizzle-orm"; + +import z from "zod"; +import { TRPCError } from "@trpc/server"; +import { monotonic_ulid } from "$lib/utils"; + +export const blobManagementRouter = trpcInstance.router({ + // #region Symmetric Key Upload + uploadSymmetricKeyAndCreateEmptyBlob: trpcInstance.procedure + .use(authMiddleware) + .input( + z.object({ + key_b64: z.string().base64(), + pubkey: z.string().ulid(), + file_name: z.string().min(3).max(100), + iv_b64: z.string().base64(), + }), + ) + .mutation(async ({ ctx, input }) => { + return await DB.transaction(async (tx_db) => { + const keyCount = ( + await tx_db + .selectDistinct({ kid: publicKeyTable.kid }) + .from(publicKeyTable) + .where( + and( + eq(publicKeyTable.kid, input.pubkey), + eq(publicKeyTable.keyOwner, ctx.user.id), + ), + ) + ).length; + + if (keyCount == 0) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "No public key uploaded", + }); + } else if (keyCount > 1) { + throw new TRPCError({ + code: "CONFLICT", + message: + "Multiple keys uploaded with KID, don't know what to do.", + }); + } else { + const [{ kid }] = await tx_db + .insert(symmetricKeyTable) + .values({ + key: input.key_b64, + publicKey: input.pubkey, + kid: monotonic_ulid(), + }) + .returning(); + + await tx_db.insert(encryptedBlobTable).values({ + kid: kid, + name: input.file_name, + state: "fresh", + owner: ctx.user.id, + iv: input.iv_b64, + }); + + return kid; + } + }); + }), + // #endregion + // #region Symmetric Key Download + downloadSymmeticKey: trpcInstance.procedure + .use(authMiddleware) + .input( + z.object({ + kid: z.string().ulid(), + }), + ) + .query(async (opts) => { + const keys = await DB.select({ + key_b64: symmetricKeyTable.key, + pubkey: symmetricKeyTable.publicKey, + }) + .from(symmetricKeyTable) + .leftJoin( + publicKeyTable, + eq(symmetricKeyTable.publicKey, publicKeyTable.kid), + ) + .where( + and( + eq(publicKeyTable.keyOwner, opts.ctx.user.id), + eq(symmetricKeyTable.kid, opts.input.kid), + ), + ); + + if (keys.length != 1) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "No key found", + }); + } else { + return { + ...keys[0], + key_b64: keys[0].key_b64 as string, + }; + } + }), + // #endregion +}); diff --git a/src/lib/server/trpc/router/index.ts b/src/lib/server/trpc/router/index.ts index 1c65484..a2f5a20 100644 --- a/src/lib/server/trpc/router/index.ts +++ b/src/lib/server/trpc/router/index.ts @@ -8,12 +8,14 @@ import { greeterRouter } from "./greeter"; import { healthRouter } from "./health"; import { userRouter } from "./user"; import { keyManagementRouter } from "./key_management"; +import { blobManagementRouter } from "./blob_management"; export const router = trpcInstance.router({ greeter: greeterRouter, health: healthRouter, user: userRouter, keyManagement: keyManagementRouter, + blobManagement: blobManagementRouter, }); export type Router = typeof router; diff --git a/src/lib/server/trpc/router/key_management.ts b/src/lib/server/trpc/router/key_management.ts index 6c3062c..541eaa0 100644 --- a/src/lib/server/trpc/router/key_management.ts +++ b/src/lib/server/trpc/router/key_management.ts @@ -5,12 +5,7 @@ import { authMiddleware } from "../middleware"; import { TRPCError } from "@trpc/server"; import { DB } from "$lib/server/db"; -import { - encryptedBlobTable, - publicKeyTable, - reservedKIDTable, - symmetricKeyTable, -} from "$lib/drizzle"; +import { publicKeyTable, reservedKIDTable } from "$lib/drizzle"; import { eq, and } from "drizzle-orm"; import { monotonic_ulid } from "$lib/utils"; @@ -287,63 +282,4 @@ export const keyManagementRouter = trpcInstance.router({ }); }), // #endregion - // #region Symmetric Key Upload - uploadSymmetricKeyAndCreateEmptyBlob: trpcInstance.procedure - .use(authMiddleware) - .input( - z.object({ - key_b64: z.string().base64(), - pubkey: z.string().ulid(), - file_name: z.string().min(3).max(100), - iv_b64: z.string().base64(), - }), - ) - .mutation(async ({ ctx, input }) => { - return await DB.transaction(async (tx_db) => { - const keyCount = ( - await tx_db - .selectDistinct({ kid: publicKeyTable.kid }) - .from(publicKeyTable) - .where( - and( - eq(publicKeyTable.kid, input.pubkey), - eq(publicKeyTable.keyOwner, ctx.user.id), - ), - ) - ).length; - - if (keyCount == 0) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "No public key uploaded", - }); - } else if (keyCount > 1) { - throw new TRPCError({ - code: "CONFLICT", - message: - "Multiple keys uploaded with KID, don't know what to do.", - }); - } else { - const [{ kid }] = await tx_db - .insert(symmetricKeyTable) - .values({ - key: input.key_b64, - publicKey: input.pubkey, - kid: monotonic_ulid(), - }) - .returning(); - - await tx_db.insert(encryptedBlobTable).values({ - kid: kid, - name: input.file_name, - state: "fresh", - owner: ctx.user.id, - iv: input.iv_b64, - }); - - return kid; - } - }); - }), - // #endregion }); diff --git a/src/lib/server/trpc/router/user.ts b/src/lib/server/trpc/router/user.ts index 31c5972..431baa2 100644 --- a/src/lib/server/trpc/router/user.ts +++ b/src/lib/server/trpc/router/user.ts @@ -2,14 +2,66 @@ import { trpcInstance } from "./init"; import { authMiddleware } from "../middleware"; +import { DB } from "$lib/server/db"; +import { + encryptedBlobTable, + publicKeyTable, + symmetricKeyTable, +} from "$lib/drizzle"; +import { eq, and, sql, inArray } from "drizzle-orm"; +import { z } from "zod"; + export const userRouter = trpcInstance.router({ getUploadedFiles: trpcInstance.procedure .use(authMiddleware) - .query(async () => { - await new Promise((res) => setTimeout(res, 1000)); + .query(async (opts) => { + DB.select({ + kid: encryptedBlobTable.name, + }) + .from(encryptedBlobTable) + .where(eq(encryptedBlobTable.owner, opts.ctx.user.id)); + }), - return { - uploadedFiles: Math.floor(Math.random() * 100), - }; + fetchUploadedFileMetadata: trpcInstance.procedure + .use(authMiddleware) + .input( + z.object({ + kids: z.array(z.string().ulid()), + }), + ) + .query(async (opts) => { + return ( + await DB.select({ + name: encryptedBlobTable.name, + size: sql`length(${encryptedBlobTable.blob})`, + id: encryptedBlobTable.kid, + owner: encryptedBlobTable.owner, + iv: encryptedBlobTable.iv, + }) + .from(encryptedBlobTable) + .leftJoin( + symmetricKeyTable, + eq(symmetricKeyTable.kid, encryptedBlobTable.kid), + ) + .leftJoin( + publicKeyTable, + eq(symmetricKeyTable.publicKey, publicKeyTable.kid), + ) + .where( + and( + and( + eq(publicKeyTable.keyOwner, opts.ctx.user.id), + inArray(publicKeyTable.kid, opts.input.kids), + ), + eq(encryptedBlobTable.state, "up"), + ), + ) + ).map((value) => { + return { + ...value, + owner: value.owner === opts.ctx.user.id, + iv: value.iv as string, + }; + }); }), }); diff --git a/src/routes/(app)/app/+page.svelte b/src/routes/(app)/app/+page.svelte index 72b0967..29f35ae 100644 --- a/src/routes/(app)/app/+page.svelte +++ b/src/routes/(app)/app/+page.svelte @@ -1,105 +1,53 @@
-
-

Folders

-
- - - - - + {#if $userBlobsQuery.isLoading} +
+ +
+ {:else if $userBlobsQuery.isError} +
+ + Failed to Load
-
- - - - - - - +
+ {#each $userBlobsQuery.data! as file (file.id)} + + {/each} +
+ {/if}
diff --git a/src/routes/(app)/uploadFileFAB.svelte b/src/routes/(app)/uploadFileFAB.svelte index dd7f1ff..6911fb6 100644 --- a/src/routes/(app)/uploadFileFAB.svelte +++ b/src/routes/(app)/uploadFileFAB.svelte @@ -30,6 +30,7 @@ import { filesize } from "filesize"; const rpc = trpc($page); + const utils = rpc.createUtils(); let dialogOpen = $state(false); @@ -47,10 +48,12 @@ if (file_element) file_element.value = ""; keyToUse = ""; isUploading = false; + + utils.user.fetchUploadedFileMetadata.refetch(); }; const startEverythingMutation = - rpc.keyManagement.uploadSymmetricKeyAndCreateEmptyBlob.createMutation(); + rpc.blobManagement.uploadSymmetricKeyAndCreateEmptyBlob.createMutation(); let keyList: ReturnType[] = $state([]); @@ -59,39 +62,48 @@ isUploading = true; - console.group("Uploading Blob"); - console.log("generating key"); - - const generated_key = await generate_symmetrical_key(); - const wrapping_key = await import_public_key(keyToUse); - - const wrapped_key = await wrap_symmetrical_key( - wrapping_key, - generated_key, - ); - const iv = window.crypto.getRandomValues(new Uint8Array(96)); - - console.log("uploading sym key"); - - const file_id = await $startEverythingMutation.mutateAsync({ - file_name: file.name, - key_b64: btoa(String.fromCharCode(...new Uint8Array(wrapped_key))), - iv_b64: btoa(String.fromCharCode(...new Uint8Array(iv))), - pubkey: keyToUse, - }); - - console.log("encrypting and uploading call"); - - const encryped_size = await encryptAndUpload( - iv, - generated_key, - file_id, - ); + try { + console.group("Uploading Blob"); + console.log("generating key"); + + const generated_key = await generate_symmetrical_key(); + const wrapping_key = await import_public_key(keyToUse); + + const wrapped_key = await wrap_symmetrical_key( + wrapping_key, + generated_key, + ); + const iv = window.crypto.getRandomValues(new Uint8Array(96)); + + console.log("uploading sym key"); + + const file_id = await $startEverythingMutation.mutateAsync({ + file_name: file.name, + key_b64: btoa( + String.fromCharCode(...new Uint8Array(wrapped_key)), + ), + iv_b64: btoa(String.fromCharCode(...new Uint8Array(iv))), + pubkey: keyToUse, + }); + + console.log("encrypting and uploading call"); + + const encryped_size = await encryptAndUpload( + iv, + generated_key, + file_id, + ); + + toast.success( + `Uploaded file ${file.name} (${filesize(encryped_size)})`, + ); + closeAndReset(); + } catch (e) { + console.error(e); + toast.error("failed to upload file"); + } - toast.success( - `Uploaded file ${file.name} (${filesize(encryped_size)})`, - ); - closeAndReset(); + console.groupEnd(); }; const encryptAndUpload = async ( @@ -137,8 +149,6 @@ console.info("uploaded success"); } - console.groupEnd(); - return encrypted_size; }; diff --git a/src/routes/api/blobs/[blob_id]/+server.ts b/src/routes/api/blobs/[blob_id]/+server.ts index 9a758a5..04ab413 100644 --- a/src/routes/api/blobs/[blob_id]/+server.ts +++ b/src/routes/api/blobs/[blob_id]/+server.ts @@ -76,3 +76,40 @@ export const POST = (async ({ params, locals, request }: RequestEvent) => { }); } }) satisfies RequestHandler; + +/** + * Since the blobs are encrypted, we don't really care about trying to protect them + * + * So we'll just make sure the user is logged in + */ +export const GET = (async ({ params, locals }: RequestEvent) => { + const { blob_id } = params; + + if (!ULID_SCHEMA.safeParse(blob_id).success) { + return new Response(null, { + status: 400, + }); + } + + if (!(locals.session && locals.user)) { + return new Response(null, { + status: 401, + }); + } + + const result = await DB.select({ + blob: encryptedBlobTable.blob, + }) + .from(encryptedBlobTable) + .where(eq(encryptedBlobTable.kid, blob_id)); + + if (result.length === 1) { + return new Response(result[0].blob as Blob, { + status: 200, + }); + } else { + return new Response(null, { + status: 404, + }); + } +}) satisfies RequestHandler;