diff --git a/server/prisma/migrations/20220828191812_add_unique_constraint_embeds/migration.sql b/server/prisma/migrations/20220828191812_add_unique_constraint_embeds/migration.sql new file mode 100644 index 0000000..4d5a6fe --- /dev/null +++ b/server/prisma/migrations/20220828191812_add_unique_constraint_embeds/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[note_id,embed_id]` on the table `EncryptedEmbed` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "EncryptedEmbed_note_id_embed_id_key" ON "EncryptedEmbed"("note_id", "embed_id"); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 9cd0ff8..32dabb5 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -28,6 +28,8 @@ model EncryptedEmbed { hmac String size_bytes Int note EncryptedNote @relation(fields: [note_id], references: [id]) + + @@unique([note_id, embed_id], name: "noteId_embedId") } model event { diff --git a/server/src/db/embed.dao.integration.test.ts b/server/src/db/embed.dao.integration.test.ts new file mode 100644 index 0000000..a85094e --- /dev/null +++ b/server/src/db/embed.dao.integration.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from "vitest"; +import type { EncryptedEmbed, EncryptedNote } from "@prisma/client"; +import { getEmbed, createEmbed } from "./embed.dao"; +import { createNote } from "./note.dao"; + +const VALID_CIPHERTEXT = Buffer.from("sample_ciphertext").toString("base64"); + +describe("Reading and writing embeds", () => { + it("Should write embeds for existing note", async () => { + const note = await createNote({ + ciphertext: "test", + hmac: "test", + crypto_version: "v2", + } as EncryptedNote); + + const embed = { + note_id: note.id, + embed_id: "embed_id", + hmac: "hmac", + ciphertext: VALID_CIPHERTEXT, + }; + + const res = await createEmbed(embed); + + expect(res.note_id).toEqual(embed.note_id); + expect(res.embed_id).toEqual(embed.embed_id); + expect(res.hmac).toEqual(embed.hmac); + expect(res.id).not.toBeNull(); + expect(res.id.length).toBeGreaterThan(0); + expect(res.ciphertext.byteLength).toBeGreaterThan(0); + }); + + it("Should throw if note_id does not refer to existing note", async () => { + const embed = { + note_id: "note_id", + embed_id: "embed_id", + hmac: "hmac", + ciphertext: VALID_CIPHERTEXT, + }; + + await expect(createEmbed(embed)).rejects.toThrowError(); + }); + + it("Should throw if embed_id is not unique", async () => { + const note = await createNote({ + ciphertext: "test", + hmac: "test", + crypto_version: "v2", + } as EncryptedNote); + + const embed = { + note_id: note.id, + embed_id: "embed_id", + hmac: "hmac", + ciphertext: VALID_CIPHERTEXT, + }; + + await createEmbed(embed); // embed 1 + await expect(createEmbed(embed)).rejects.toThrowError(); // duplicate embed + }); + + it("Should read embeds for existing note", async () => { + const note = await createNote({ + ciphertext: "test", + hmac: "test", + crypto_version: "v2", + } as EncryptedNote); + + const embed = { + note_id: note.id, + embed_id: "embed_id", + hmac: "hmac", + ciphertext: VALID_CIPHERTEXT, + }; + + await createEmbed(embed); + const res = await getEmbed(note.id, embed.embed_id); + + expect(res).not.toBeNull(); + expect(res?.note_id).toEqual(embed.note_id); + expect(res?.embed_id).toEqual(embed.embed_id); + expect(res?.hmac).toEqual(embed.hmac); + expect(res?.ciphertext).toEqual(embed.ciphertext); + }); +}); diff --git a/server/src/db/embed.dao.ts b/server/src/db/embed.dao.ts new file mode 100644 index 0000000..2b5d42a --- /dev/null +++ b/server/src/db/embed.dao.ts @@ -0,0 +1,49 @@ +import { EncryptedEmbed } from "@prisma/client"; +import { BufferToBase64, base64ToBuffer } from "../util"; +import prisma from "./client"; + +export interface EncryptedEmbedDTO { + note_id: string; + embed_id: string; + ciphertext: string; // in base64 + hmac: string; +} + +export async function getEmbed( + noteId: string, + embedId: string +): Promise { + const embed = await prisma.encryptedEmbed.findUnique({ + where: { + noteId_embedId: { + note_id: noteId, + embed_id: embedId, + }, + }, + }); + + if (!embed) return null; + + console.log(embed.ciphertext.byteLength, embed.size_bytes); + + return { + note_id: embed.note_id, + embed_id: embed.embed_id, + hmac: embed.hmac, + ciphertext: BufferToBase64(embed.ciphertext), + }; +} + +export async function createEmbed( + embed: EncryptedEmbedDTO +): Promise { + const cipher_buf = base64ToBuffer(embed.ciphertext); + const data = { + note_id: embed.note_id, + embed_id: embed.embed_id, + hmac: embed.hmac, + ciphertext: cipher_buf, + size_bytes: cipher_buf.byteLength, + } as EncryptedEmbed; + return prisma.encryptedEmbed.create({ data }); +} diff --git a/server/src/util.ts b/server/src/util.ts index 9305d36..ad9660f 100644 --- a/server/src/util.ts +++ b/server/src/util.ts @@ -13,11 +13,11 @@ export function getConnectingIp(req: Request): string { } // base64 to array buffer (Node JS api, so don't use atob or btoa) -export function base64ToArrayBuffer(base64: string): ArrayBuffer { +export function base64ToBuffer(base64: string): Buffer { return Buffer.from(base64, "base64"); } // array buffer to base64 (Node JS api, so don't use atob or btoa) -export function arrayBufferToBase64(buffer: ArrayBuffer): string { +export function BufferToBase64(buffer: Buffer): string { return Buffer.from(buffer).toString("base64"); } diff --git a/server/src/util.unit.test.ts b/server/src/util.unit.test.ts index 6c88af1..c038246 100644 --- a/server/src/util.unit.test.ts +++ b/server/src/util.unit.test.ts @@ -1,8 +1,8 @@ import { describe, it, expect } from "vitest"; import { addDays, - arrayBufferToBase64, - base64ToArrayBuffer, + BufferToBase64, + base64ToBuffer, getConnectingIp, } from "./util"; @@ -18,7 +18,7 @@ describe("converting to/from base64", () => { it("Should convert a base64 string to an array buffer", () => { const base64 = "EjRWeJA="; const expectedBuffer = new Uint8Array([18, 52, 86, 120, 144]); - expect(new Uint8Array(base64ToArrayBuffer(base64))).toStrictEqual( + expect(new Uint8Array(base64ToBuffer(base64))).toStrictEqual( expectedBuffer ); }); @@ -26,6 +26,6 @@ describe("converting to/from base64", () => { it("Should convert an array buffer to a base64 string", () => { const buffer = new Uint8Array([18, 52, 86, 120, 144]); const expectedBase64 = "EjRWeJA="; - expect(arrayBufferToBase64(buffer)).toEqual(expectedBase64); + expect(BufferToBase64(buffer)).toEqual(expectedBase64); }); });