From e6dc2d849aeba5b5d50b188f9756a9a74d872689 Mon Sep 17 00:00:00 2001 From: Maxime Cannoodt Date: Mon, 21 Nov 2022 08:25:45 +0100 Subject: [PATCH] write delete endpoint --- plugin | 2 +- .../note/note.delete.controller.ts | 73 +++++++++++++++++++ .../note/note.delete.controller.unit.test.ts | 0 .../controllers/note/note.post.controller.ts | 57 +-------------- .../note/note.post.controller.unit.test.ts | 3 +- server/src/controllers/note/note.router.ts | 2 + server/src/db/note.dao.ts | 6 ++ server/src/lib/checkUserId.ts | 21 ++++++ server/src/logging/EventLogger.ts | 22 +++++- server/src/validation/Request.ts | 40 ++++++++++ 10 files changed, 169 insertions(+), 57 deletions(-) create mode 100644 server/src/controllers/note/note.delete.controller.ts create mode 100644 server/src/controllers/note/note.delete.controller.unit.test.ts create mode 100644 server/src/lib/checkUserId.ts create mode 100644 server/src/validation/Request.ts diff --git a/plugin b/plugin index 4ead2c6..961634c 160000 --- a/plugin +++ b/plugin @@ -1 +1 @@ -Subproject commit 4ead2c609f30a2ad64b45bf9c19b4ba4e1de54e9 +Subproject commit 961634c1c45c1e3227617eac0700a9d0b1cf6417 diff --git a/server/src/controllers/note/note.delete.controller.ts b/server/src/controllers/note/note.delete.controller.ts new file mode 100644 index 0000000..ce441a3 --- /dev/null +++ b/server/src/controllers/note/note.delete.controller.ts @@ -0,0 +1,73 @@ +import { validateOrReject, ValidationError } from "class-validator"; +import { NextFunction, Request, Response } from "express"; +import { deleteNote, getNote } from "../../db/note.dao"; +import checkId from "../../lib/checkUserId"; +import EventLogger, { WriteEvent } from "../../logging/EventLogger"; +import { getConnectingIp, getNoteSize } from "../../util"; +import { NoteDeleteRequest } from "../../validation/Request"; + +export async function deleteNoteController( + req: Request, + res: Response, + next: NextFunction +): Promise { + const event: WriteEvent = { + success: false, + host: getConnectingIp(req), + user_id: req.body.user_id, + user_plugin_version: req.body.plugin_version, + }; + + // Validate request body + const noteDeleteRequest = new NoteDeleteRequest(); + Object.assign(noteDeleteRequest, req.body); + try { + await validateOrReject(noteDeleteRequest); + } catch (_err: any) { + const err = _err as ValidationError; + res.status(400).send(err.toString()); + event.error = err.toString(); + await EventLogger.deleteEvent(event); + return; + } + + // Validate user ID, if present + if (noteDeleteRequest.user_id && !checkId(noteDeleteRequest.user_id)) { + console.log("invalid user id"); + res.status(400).send("Invalid user id (checksum failed)"); + event.error = "Invalid user id (checksum failed)"; + EventLogger.writeEvent(event); + return; + } + + // get note from db + const note = await getNote(req.params.id); + if (!note) { + res.status(404).send("Note not found"); + event.error = "Note not found"; + await EventLogger.deleteEvent(event); + return; + } + + // Validate secret token + if (note.secret_token !== req.body.secret_token) { + res.status(401).send("Invalid token"); + event.error = "Invalid secret token"; + await EventLogger.deleteEvent(event); + return; + } + + // Delete note + try { + await deleteNote(note.id); + res.status(200); + event.success = true; + event.note_id = note.id; + event.size_bytes = getNoteSize(note); + await EventLogger.deleteEvent(event); + } catch (err) { + event.error = (err as Error).toString(); + await EventLogger.deleteEvent(event); + next(err); + } +} diff --git a/server/src/controllers/note/note.delete.controller.unit.test.ts b/server/src/controllers/note/note.delete.controller.unit.test.ts new file mode 100644 index 0000000..e69de29 diff --git a/server/src/controllers/note/note.post.controller.ts b/server/src/controllers/note/note.post.controller.ts index 1ddd413..b85579e 100644 --- a/server/src/controllers/note/note.post.controller.ts +++ b/server/src/controllers/note/note.post.controller.ts @@ -1,47 +1,16 @@ import { EncryptedNote } from "@prisma/client"; import { NextFunction, Request, Response } from "express"; -import { crc16 as crc } from "crc"; import { createNote } from "../../db/note.dao"; import { addDays, getConnectingIp, getNoteSize } from "../../util"; import EventLogger, { WriteEvent } from "../../logging/EventLogger"; -import { - validateOrReject, - IsBase64, - IsHexadecimal, - IsNotEmpty, - ValidateIf, - ValidationError, - Matches, -} from "class-validator"; +import { validateOrReject, ValidationError } from "class-validator"; import { generateToken } from "../../crypto/GenerateToken"; +import { NotePostRequest } from "../../validation/Request"; +import checkId from "../../lib/checkUserId"; /** * Request body for creating a note */ -export class NotePostRequest { - @IsBase64() - @IsNotEmpty() - ciphertext: string | undefined; - - @IsBase64() - @ValidateIf((o) => !o.iv) - hmac?: string | undefined; - - @IsBase64() - @ValidateIf((o) => !o.hmac) - iv?: string | undefined; - - @ValidateIf((o) => o.user_id != null) - @IsHexadecimal() - user_id: string | undefined; - - @ValidateIf((o) => o.plugin_version != null) - @Matches("^[0-9]+\\.[0-9]+\\.[0-9]+$") - plugin_version: string | undefined; - - @Matches("^v[0-9]+$") - crypto_version: string = "v1"; -} export async function postNoteController( req: Request, @@ -110,23 +79,3 @@ export async function postNoteController( next(err); }); } - -/** - * @param id {string} a 16 character base16 string with 12 random characters and 4 CRC characters - * @returns {boolean} true if the id is valid, false otherwise - */ -function checkId(id: string): boolean { - // check length - if (id.length !== 16) { - return false; - } - // extract the random number and the checksum - const random = id.slice(0, 12); - const checksum = id.slice(12, 16); - - // compute the CRC of the random number - const computedChecksum = crc(random).toString(16).padStart(4, "0"); - - // compare the computed checksum with the one in the id - return computedChecksum === checksum; -} diff --git a/server/src/controllers/note/note.post.controller.unit.test.ts b/server/src/controllers/note/note.post.controller.unit.test.ts index c15af2f..624e663 100644 --- a/server/src/controllers/note/note.post.controller.unit.test.ts +++ b/server/src/controllers/note/note.post.controller.unit.test.ts @@ -3,7 +3,8 @@ import supertest from "supertest"; import { vi, describe, it, beforeEach, afterEach, expect } from "vitest"; import * as noteDao from "../../db/note.dao"; import EventLogger from "../../logging/EventLogger"; -import { NotePostRequest, postNoteController } from "./note.post.controller"; +import { NotePostRequest } from "../../validation/Request"; +import { postNoteController } from "./note.post.controller"; vi.mock("../../db/note.dao"); vi.mock("../../logging/EventLogger"); diff --git a/server/src/controllers/note/note.router.ts b/server/src/controllers/note/note.router.ts index 180e3e4..8a1001d 100644 --- a/server/src/controllers/note/note.router.ts +++ b/server/src/controllers/note/note.router.ts @@ -1,5 +1,6 @@ import express from "express"; import rateLimit from "express-rate-limit"; +import { deleteNoteController } from "./note.delete.controller"; import { getNoteController } from "./note.get.controller"; import { postNoteController } from "./note.post.controller"; @@ -25,3 +26,4 @@ const getRateLimit = rateLimit({ notesRoute.use(jsonParser); notesRoute.post("", postRateLimit, postNoteController); notesRoute.get("/:id", getRateLimit, getNoteController); +notesRoute.delete("/:id", getRateLimit, deleteNoteController); diff --git a/server/src/db/note.dao.ts b/server/src/db/note.dao.ts index e91a573..817cf60 100644 --- a/server/src/db/note.dao.ts +++ b/server/src/db/note.dao.ts @@ -23,6 +23,12 @@ export async function getExpiredNotes(): Promise { }); } +export async function deleteNote(noteId: string): Promise { + return prisma.encryptedNote.delete({ + where: { id: noteId }, + }); +} + export async function deleteNotes(noteIds: string[]): Promise { return prisma.encryptedNote .deleteMany({ diff --git a/server/src/lib/checkUserId.ts b/server/src/lib/checkUserId.ts new file mode 100644 index 0000000..6aa4942 --- /dev/null +++ b/server/src/lib/checkUserId.ts @@ -0,0 +1,21 @@ +import { crc16 as crc } from "crc"; + +/** + * @param id {string} a 16 character base16 string with 12 random characters and 4 CRC characters + * @returns {boolean} true if the id is valid, false otherwise + */ +export default function checkId(id: string): boolean { + // check length + if (id.length !== 16) { + return false; + } + // extract the random number and the checksum + const random = id.slice(0, 12); + const checksum = id.slice(12, 16); + + // compute the CRC of the random number + const computedChecksum = crc(random).toString(16).padStart(4, "0"); + + // compare the computed checksum with the one in the id + return computedChecksum === checksum; +} diff --git a/server/src/logging/EventLogger.ts b/server/src/logging/EventLogger.ts index 4671c11..996c64a 100644 --- a/server/src/logging/EventLogger.ts +++ b/server/src/logging/EventLogger.ts @@ -5,10 +5,12 @@ import logger from "./logger"; export enum EventType { WRITE = "WRITE", READ = "READ", + DELETE = "DELETE", + UPDATE = "UPDATE", PURGE = "PURGE", } -interface Event { +export interface Event { success: boolean; error?: string; } @@ -26,6 +28,10 @@ export interface WriteEvent extends ClientEvent { expire_window_days?: number; } +interface DeleteEvent extends ClientEvent {} + +interface UpdateEvent extends ClientEvent {} + interface ReadEvent extends ClientEvent {} interface PurgeEvent extends Event { @@ -54,6 +60,20 @@ export default class EventLogger { }); } + public static deleteEvent(event: DeleteEvent): Promise { + this.printError(event); + return prisma.event.create({ + data: { type: EventType.DELETE, ...event }, + }); + } + + public static updateEvent(event: UpdateEvent): Promise { + this.printError(event); + return prisma.event.create({ + data: { type: EventType.UPDATE, ...event }, + }); + } + public static purgeEvent(event: PurgeEvent): Promise { this.printError(event); return prisma.event.create({ diff --git a/server/src/validation/Request.ts b/server/src/validation/Request.ts new file mode 100644 index 0000000..dc0f846 --- /dev/null +++ b/server/src/validation/Request.ts @@ -0,0 +1,40 @@ +import { + IsBase64, + IsHexadecimal, + IsNotEmpty, + Matches, + ValidateIf, +} from "class-validator"; + +abstract class NoteRequestBody { + @ValidateIf((o) => o.user_id != null) + @IsHexadecimal() + user_id: string | undefined; + + @ValidateIf((o) => o.plugin_version != null) + @Matches("^[0-9]+\\.[0-9]+\\.[0-9]+$") + plugin_version: string | undefined; +} + +export class NotePostRequest extends NoteRequestBody { + @IsBase64() + @IsNotEmpty() + ciphertext: string | undefined; + + @IsBase64() + @ValidateIf((o) => !o.iv) + hmac?: string | undefined; + + @IsBase64() + @ValidateIf((o) => !o.hmac) + iv?: string | undefined; + + @Matches("^v[0-9]+$") + crypto_version: string = "v1"; +} + +export class NoteDeleteRequest extends NoteRequestBody { + @IsBase64() + @IsNotEmpty() + secret_token: string | undefined; +}