feat: 🔒 Upgrade encryption model to use GCM cipher and randomized IV.

This commit is contained in:
Maxime Cannoodt 2022-11-13 15:47:10 +01:00
parent 6f3552ce59
commit 8bceeaf4c0
9 changed files with 104 additions and 26 deletions

View File

@ -0,0 +1,16 @@
-- RedefineTables
PRAGMA foreign_keys=OFF;
CREATE TABLE "new_EncryptedNote" (
"id" TEXT NOT NULL PRIMARY KEY,
"insert_time" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"expire_time" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"ciphertext" TEXT NOT NULL,
"hmac" TEXT,
"iv" TEXT,
"crypto_version" TEXT NOT NULL DEFAULT 'v1'
);
INSERT INTO "new_EncryptedNote" ("ciphertext", "crypto_version", "expire_time", "hmac", "id", "insert_time") SELECT "ciphertext", "crypto_version", "expire_time", "hmac", "id", "insert_time" FROM "EncryptedNote";
DROP TABLE "EncryptedNote";
ALTER TABLE "new_EncryptedNote" RENAME TO "EncryptedNote";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View File

@ -15,7 +15,8 @@ model EncryptedNote {
insert_time DateTime @default(now())
expire_time DateTime @default(now())
ciphertext String
hmac String
hmac String?
iv String?
crypto_version String @default("v1")
}

View File

@ -1,7 +1,7 @@
import { NextFunction, Request, Response } from "express";
import { getExpiredNoteFilter } from "../../lib/expiredNoteFilter";
import EventLogger from "../../logging/EventLogger";
import { getConnectingIp } from "../../util";
import { getConnectingIp, getNoteSize } from "../../util";
import { getNote } from "../../db/note.dao";
export async function getNoteController(
req: Request,
@ -16,7 +16,7 @@ export async function getNoteController(
success: true,
host: ip,
note_id: note.id,
size_bytes: note.ciphertext.length + note.hmac.length,
size_bytes: getNoteSize(note),
});
res.send(note);
} else {

View File

@ -2,7 +2,7 @@ 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 } from "../../util";
import { addDays, getConnectingIp, getNoteSize } from "../../util";
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
import {
validateOrReject,
@ -23,8 +23,12 @@ export class NotePostRequest {
ciphertext: string | undefined;
@IsBase64()
@IsNotEmpty()
hmac: string | undefined;
@ValidateIf((o) => !o.iv)
hmac?: string | undefined;
@IsBase64()
@ValidateIf((o) => !o.hmac)
iv?: string | undefined;
@ValidateIf((o) => o.user_id != null)
@IsHexadecimal()
@ -77,6 +81,7 @@ export async function postNoteController(
const note = {
ciphertext: notePostRequest.ciphertext as string,
hmac: notePostRequest.hmac as string,
iv: notePostRequest.iv as string,
expire_time: addDays(new Date(), EXPIRE_WINDOW_DAYS),
crypto_version: notePostRequest.crypto_version,
} as EncryptedNote;
@ -86,7 +91,7 @@ export async function postNoteController(
.then(async (savedNote) => {
event.success = true;
event.note_id = savedNote.id;
event.size_bytes = savedNote.ciphertext.length + savedNote.hmac.length;
event.size_bytes = getNoteSize(note);
event.expire_window_days = EXPIRE_WINDOW_DAYS;
await EventLogger.writeEvent(event);
res.json({

View File

@ -1,5 +1,6 @@
import { event } from "@prisma/client";
import prisma from "../db/client";
import logger from "./logger";
export enum EventType {
WRITE = "WRITE",
@ -33,19 +34,28 @@ interface PurgeEvent extends Event {
}
export default class EventLogger {
private static printError(event: Event) {
if (event.error) {
logger.error(event.error);
}
}
public static writeEvent(event: WriteEvent): Promise<event> {
this.printError(event);
return prisma.event.create({
data: { type: EventType.WRITE, ...event },
});
}
public static readEvent(event: ReadEvent): Promise<event> {
this.printError(event);
return prisma.event.create({
data: { type: EventType.READ, ...event },
});
}
public static purgeEvent(event: PurgeEvent): Promise<event> {
this.printError(event);
return prisma.event.create({
data: { type: EventType.PURGE, ...event },
});

View File

@ -2,6 +2,7 @@ import { deleteNotes, getExpiredNotes } from "../db/note.dao";
import { getExpiredNoteFilter } from "../lib/expiredNoteFilter";
import EventLogger from "../logging/EventLogger";
import logger from "../logging/logger";
import { getNoteSize } from "../util";
export async function deleteExpiredNotes(): Promise<number> {
logger.info("[Cleanup] Cleaning up expired notes...");
@ -11,14 +12,14 @@ export async function deleteExpiredNotes(): Promise<number> {
.then(async (deleteCount) => {
const logs = toDelete.map(async (note) => {
logger.info(
`[Cleanup] Deleted note ${note.id} with size ${
note.ciphertext.length + note.hmac.length
} bytes`
`[Cleanup] Deleted note ${note.id} with size ${getNoteSize(
note
)} bytes`
);
return EventLogger.purgeEvent({
success: true,
note_id: note.id,
size_bytes: note.ciphertext.length + note.hmac.length,
size_bytes: getNoteSize(note),
});
});
await Promise.all(logs);

View File

@ -1,3 +1,4 @@
import { EncryptedNote } from "@prisma/client";
import { Request } from "express";
export function addDays(date: Date, days: number): Date {
@ -11,3 +12,11 @@ export function getConnectingIp(req: Request): string {
req.headers["X-Forwarded-For"] ||
req.socket.remoteAddress) as string;
}
export function getNoteSize(
note: Pick<EncryptedNote, "ciphertext" | "hmac" | "iv">
) {
return (
note.ciphertext.length + (note.hmac?.length ?? 0) + (note.iv?.length ?? 0)
);
}

View File

@ -2,16 +2,31 @@
import { AES, enc, HmacSHA256 } from 'crypto-js';
export async function decrypt(
cryptData: { ciphertext: string; hmac: string; key: string },
version: string
): Promise<string> {
type CryptData = {
ciphertext: string;
key: string;
iv?: string;
hmac?: string;
};
type CryptData_v1 = CryptData & {
hmac: string;
};
type CryptData_v3 = CryptData & {
iv: string;
};
export async function decrypt(cryptData: CryptData, version: string): Promise<string> {
console.debug(`decrypting with crypto suite ${version}`);
if (version === 'v1') {
return decrypt_v1(cryptData);
return decrypt_v1(cryptData as CryptData_v1);
}
if (version === 'v2') {
return decrypt_v2(cryptData);
return decrypt_v2(cryptData as CryptData_v1);
}
if (version === 'v3') {
return decrypt_v3(cryptData as CryptData_v3);
}
throw new Error(`Unsupported crypto version: ${version}`);
}
@ -53,15 +68,37 @@ export async function decrypt_v2(cryptData: {
const md = await window.crypto.subtle.decrypt(
{ name: 'AES-CBC', iv: new Uint8Array(16) },
await _getAesKey(secret),
await _getAesCbcKey(secret),
ciphertext_buf
);
return new TextDecoder().decode(md);
}
function _getAesKey(secret: ArrayBuffer): Promise<CryptoKey> {
export async function decrypt_v3(cryptData: {
ciphertext: string;
iv: string;
key: string;
}): Promise<string> {
const secret = base64ToArrayBuffer(cryptData.key);
const ciphertext_buf = base64ToArrayBuffer(cryptData.ciphertext);
const iv_buf = base64ToArrayBuffer(cryptData.iv);
const md = await window.crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: iv_buf },
await _getAesGcmKey(secret),
ciphertext_buf
);
return new TextDecoder().decode(md);
}
function _getAesCbcKey(secret: ArrayBuffer): Promise<CryptoKey> {
return window.crypto.subtle.importKey('raw', secret, { name: 'AES-CBC', length: 256 }, false, [
'encrypt',
'decrypt'
]);
}
function _getAesGcmKey(secret: ArrayBuffer): Promise<CryptoKey> {
return window.crypto.subtle.importKey('raw', secret, { name: 'AES-GCM', length: 256 }, false, [
'decrypt'
]);
}

View File

@ -8,19 +8,18 @@
import RawRenderer from '$lib/components/RawRenderer.svelte';
import LogoDocument from 'svelte-icons/md/MdUndo.svelte';
import Dismissable from '$lib/components/Dismissable.svelte';
import type { PageData } from './$types';
// Auto-loaded from [id].ts endpoint
/** @type {import('./$types').PageData} */
export let data;
export let data: PageData;
let { note } = data;
let plaintext: string;
let timeString: string;
let decryptFailed = false;
let showRaw = false;
onMount(() => {
if (browser) {
if (browser && note) {
const key = location.hash.slice(1);
decrypt({ ...note, key }, note.crypto_version)
.then((value) => (plaintext = value))
@ -28,7 +27,7 @@
}
});
$: if (note.insert_time) {
$: if (note?.insert_time) {
const diff_ms = new Date().valueOf() - new Date(note.insert_time).valueOf();
timeString = msToString(diff_ms);
}