feat: 🔒 Upgrade encryption model to use GCM cipher and randomized IV.
This commit is contained in:
parent
6f3552ce59
commit
8bceeaf4c0
@ -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;
|
@ -15,7 +15,8 @@ model EncryptedNote {
|
|||||||
insert_time DateTime @default(now())
|
insert_time DateTime @default(now())
|
||||||
expire_time DateTime @default(now())
|
expire_time DateTime @default(now())
|
||||||
ciphertext String
|
ciphertext String
|
||||||
hmac String
|
hmac String?
|
||||||
|
iv String?
|
||||||
crypto_version String @default("v1")
|
crypto_version String @default("v1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { getExpiredNoteFilter } from "../../lib/expiredNoteFilter";
|
import { getExpiredNoteFilter } from "../../lib/expiredNoteFilter";
|
||||||
import EventLogger from "../../logging/EventLogger";
|
import EventLogger from "../../logging/EventLogger";
|
||||||
import { getConnectingIp } from "../../util";
|
import { getConnectingIp, getNoteSize } from "../../util";
|
||||||
import { getNote } from "../../db/note.dao";
|
import { getNote } from "../../db/note.dao";
|
||||||
export async function getNoteController(
|
export async function getNoteController(
|
||||||
req: Request,
|
req: Request,
|
||||||
@ -16,7 +16,7 @@ export async function getNoteController(
|
|||||||
success: true,
|
success: true,
|
||||||
host: ip,
|
host: ip,
|
||||||
note_id: note.id,
|
note_id: note.id,
|
||||||
size_bytes: note.ciphertext.length + note.hmac.length,
|
size_bytes: getNoteSize(note),
|
||||||
});
|
});
|
||||||
res.send(note);
|
res.send(note);
|
||||||
} else {
|
} else {
|
||||||
|
@ -2,7 +2,7 @@ import { EncryptedNote } from "@prisma/client";
|
|||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { crc16 as crc } from "crc";
|
import { crc16 as crc } from "crc";
|
||||||
import { createNote } from "../../db/note.dao";
|
import { createNote } from "../../db/note.dao";
|
||||||
import { addDays, getConnectingIp } from "../../util";
|
import { addDays, getConnectingIp, getNoteSize } from "../../util";
|
||||||
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
|
import EventLogger, { WriteEvent } from "../../logging/EventLogger";
|
||||||
import {
|
import {
|
||||||
validateOrReject,
|
validateOrReject,
|
||||||
@ -23,8 +23,12 @@ export class NotePostRequest {
|
|||||||
ciphertext: string | undefined;
|
ciphertext: string | undefined;
|
||||||
|
|
||||||
@IsBase64()
|
@IsBase64()
|
||||||
@IsNotEmpty()
|
@ValidateIf((o) => !o.iv)
|
||||||
hmac: string | undefined;
|
hmac?: string | undefined;
|
||||||
|
|
||||||
|
@IsBase64()
|
||||||
|
@ValidateIf((o) => !o.hmac)
|
||||||
|
iv?: string | undefined;
|
||||||
|
|
||||||
@ValidateIf((o) => o.user_id != null)
|
@ValidateIf((o) => o.user_id != null)
|
||||||
@IsHexadecimal()
|
@IsHexadecimal()
|
||||||
@ -77,6 +81,7 @@ export async function postNoteController(
|
|||||||
const note = {
|
const note = {
|
||||||
ciphertext: notePostRequest.ciphertext as string,
|
ciphertext: notePostRequest.ciphertext as string,
|
||||||
hmac: notePostRequest.hmac as string,
|
hmac: notePostRequest.hmac as string,
|
||||||
|
iv: notePostRequest.iv as string,
|
||||||
expire_time: addDays(new Date(), EXPIRE_WINDOW_DAYS),
|
expire_time: addDays(new Date(), EXPIRE_WINDOW_DAYS),
|
||||||
crypto_version: notePostRequest.crypto_version,
|
crypto_version: notePostRequest.crypto_version,
|
||||||
} as EncryptedNote;
|
} as EncryptedNote;
|
||||||
@ -86,7 +91,7 @@ export async function postNoteController(
|
|||||||
.then(async (savedNote) => {
|
.then(async (savedNote) => {
|
||||||
event.success = true;
|
event.success = true;
|
||||||
event.note_id = savedNote.id;
|
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;
|
event.expire_window_days = EXPIRE_WINDOW_DAYS;
|
||||||
await EventLogger.writeEvent(event);
|
await EventLogger.writeEvent(event);
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { event } from "@prisma/client";
|
import { event } from "@prisma/client";
|
||||||
import prisma from "../db/client";
|
import prisma from "../db/client";
|
||||||
|
import logger from "./logger";
|
||||||
|
|
||||||
export enum EventType {
|
export enum EventType {
|
||||||
WRITE = "WRITE",
|
WRITE = "WRITE",
|
||||||
@ -33,19 +34,28 @@ interface PurgeEvent extends Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class EventLogger {
|
export default class EventLogger {
|
||||||
|
private static printError(event: Event) {
|
||||||
|
if (event.error) {
|
||||||
|
logger.error(event.error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static writeEvent(event: WriteEvent): Promise<event> {
|
public static writeEvent(event: WriteEvent): Promise<event> {
|
||||||
|
this.printError(event);
|
||||||
return prisma.event.create({
|
return prisma.event.create({
|
||||||
data: { type: EventType.WRITE, ...event },
|
data: { type: EventType.WRITE, ...event },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static readEvent(event: ReadEvent): Promise<event> {
|
public static readEvent(event: ReadEvent): Promise<event> {
|
||||||
|
this.printError(event);
|
||||||
return prisma.event.create({
|
return prisma.event.create({
|
||||||
data: { type: EventType.READ, ...event },
|
data: { type: EventType.READ, ...event },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static purgeEvent(event: PurgeEvent): Promise<event> {
|
public static purgeEvent(event: PurgeEvent): Promise<event> {
|
||||||
|
this.printError(event);
|
||||||
return prisma.event.create({
|
return prisma.event.create({
|
||||||
data: { type: EventType.PURGE, ...event },
|
data: { type: EventType.PURGE, ...event },
|
||||||
});
|
});
|
||||||
|
@ -2,6 +2,7 @@ import { deleteNotes, getExpiredNotes } from "../db/note.dao";
|
|||||||
import { getExpiredNoteFilter } from "../lib/expiredNoteFilter";
|
import { getExpiredNoteFilter } from "../lib/expiredNoteFilter";
|
||||||
import EventLogger from "../logging/EventLogger";
|
import EventLogger from "../logging/EventLogger";
|
||||||
import logger from "../logging/logger";
|
import logger from "../logging/logger";
|
||||||
|
import { getNoteSize } from "../util";
|
||||||
|
|
||||||
export async function deleteExpiredNotes(): Promise<number> {
|
export async function deleteExpiredNotes(): Promise<number> {
|
||||||
logger.info("[Cleanup] Cleaning up expired notes...");
|
logger.info("[Cleanup] Cleaning up expired notes...");
|
||||||
@ -11,14 +12,14 @@ export async function deleteExpiredNotes(): Promise<number> {
|
|||||||
.then(async (deleteCount) => {
|
.then(async (deleteCount) => {
|
||||||
const logs = toDelete.map(async (note) => {
|
const logs = toDelete.map(async (note) => {
|
||||||
logger.info(
|
logger.info(
|
||||||
`[Cleanup] Deleted note ${note.id} with size ${
|
`[Cleanup] Deleted note ${note.id} with size ${getNoteSize(
|
||||||
note.ciphertext.length + note.hmac.length
|
note
|
||||||
} bytes`
|
)} bytes`
|
||||||
);
|
);
|
||||||
return EventLogger.purgeEvent({
|
return EventLogger.purgeEvent({
|
||||||
success: true,
|
success: true,
|
||||||
note_id: note.id,
|
note_id: note.id,
|
||||||
size_bytes: note.ciphertext.length + note.hmac.length,
|
size_bytes: getNoteSize(note),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
await Promise.all(logs);
|
await Promise.all(logs);
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { EncryptedNote } from "@prisma/client";
|
||||||
import { Request } from "express";
|
import { Request } from "express";
|
||||||
|
|
||||||
export function addDays(date: Date, days: number): Date {
|
export function addDays(date: Date, days: number): Date {
|
||||||
@ -11,3 +12,11 @@ export function getConnectingIp(req: Request): string {
|
|||||||
req.headers["X-Forwarded-For"] ||
|
req.headers["X-Forwarded-For"] ||
|
||||||
req.socket.remoteAddress) as string;
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -2,16 +2,31 @@
|
|||||||
|
|
||||||
import { AES, enc, HmacSHA256 } from 'crypto-js';
|
import { AES, enc, HmacSHA256 } from 'crypto-js';
|
||||||
|
|
||||||
export async function decrypt(
|
type CryptData = {
|
||||||
cryptData: { ciphertext: string; hmac: string; key: string },
|
ciphertext: string;
|
||||||
version: string
|
key: string;
|
||||||
): Promise<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}`);
|
console.debug(`decrypting with crypto suite ${version}`);
|
||||||
if (version === 'v1') {
|
if (version === 'v1') {
|
||||||
return decrypt_v1(cryptData);
|
return decrypt_v1(cryptData as CryptData_v1);
|
||||||
}
|
}
|
||||||
if (version === 'v2') {
|
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}`);
|
throw new Error(`Unsupported crypto version: ${version}`);
|
||||||
}
|
}
|
||||||
@ -53,15 +68,37 @@ export async function decrypt_v2(cryptData: {
|
|||||||
|
|
||||||
const md = await window.crypto.subtle.decrypt(
|
const md = await window.crypto.subtle.decrypt(
|
||||||
{ name: 'AES-CBC', iv: new Uint8Array(16) },
|
{ name: 'AES-CBC', iv: new Uint8Array(16) },
|
||||||
await _getAesKey(secret),
|
await _getAesCbcKey(secret),
|
||||||
ciphertext_buf
|
ciphertext_buf
|
||||||
);
|
);
|
||||||
return new TextDecoder().decode(md);
|
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, [
|
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'
|
'decrypt'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
@ -8,19 +8,18 @@
|
|||||||
import RawRenderer from '$lib/components/RawRenderer.svelte';
|
import RawRenderer from '$lib/components/RawRenderer.svelte';
|
||||||
import LogoDocument from 'svelte-icons/md/MdUndo.svelte';
|
import LogoDocument from 'svelte-icons/md/MdUndo.svelte';
|
||||||
import Dismissable from '$lib/components/Dismissable.svelte';
|
import Dismissable from '$lib/components/Dismissable.svelte';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
// Auto-loaded from [id].ts endpoint
|
export let data: PageData;
|
||||||
|
|
||||||
/** @type {import('./$types').PageData} */
|
|
||||||
export let data;
|
|
||||||
let { note } = data;
|
let { note } = data;
|
||||||
|
|
||||||
let plaintext: string;
|
let plaintext: string;
|
||||||
let timeString: string;
|
let timeString: string;
|
||||||
let decryptFailed = false;
|
let decryptFailed = false;
|
||||||
let showRaw = false;
|
let showRaw = false;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (browser) {
|
if (browser && note) {
|
||||||
const key = location.hash.slice(1);
|
const key = location.hash.slice(1);
|
||||||
decrypt({ ...note, key }, note.crypto_version)
|
decrypt({ ...note, key }, note.crypto_version)
|
||||||
.then((value) => (plaintext = value))
|
.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();
|
const diff_ms = new Date().valueOf() - new Date(note.insert_time).valueOf();
|
||||||
timeString = msToString(diff_ms);
|
timeString = msToString(diff_ms);
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user