Files
ihasmail-inbuxa/server/src/crypto.ts
T
jcoffey-dev 645b8b510f ihasmail 2.0: rebuild as Stalwart-first JMAP webmail
Replace the FastAPI/HTMX prototype with a Node/Hono session proxy and a
React 19/Vite SPA. Mail (conversation view, search operators, labels,
sanitised HTML, privacy image proxy, invites, undo send, templates),
calendar (month/week/day/agenda, invites, free/busy, categories,
context menus), contacts (JSContact, groups, vCard), files, Sieve filter
builder (incl. filter-from-message with retroactive apply), vacation,
identities with default + Reply-To, PWA/mobile layout, push via SSE,
in-memory mock Stalwart for dev, Docker + CI.
2026-08-23 01:07:13 -07:00

57 lines
1.8 KiB
TypeScript

import {
createCipheriv,
createDecipheriv,
createHash,
hkdfSync,
randomBytes,
timingSafeEqual,
} from "node:crypto";
/**
* Credentials are sealed with a key derived from the per-session cookie secret
* combined with the app secret. The server persists only the ciphertext plus a
* hash of the cookie secret, so a stolen session file cannot be turned back into
* passwords without also holding the users' cookies.
*/
export function deriveKey(cookieSecret: string, appSecret: string, salt: Buffer): Buffer {
const ikm = Buffer.from(`${cookieSecret}${appSecret}`, "utf8");
return Buffer.from(hkdfSync("sha256", ikm, salt, "ihasmail-session-v1", 32));
}
export function seal(plaintext: string, key: Buffer): string {
const iv = randomBytes(12);
const cipher = createCipheriv("aes-256-gcm", key, iv);
const ct = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
const tag = cipher.getAuthTag();
return Buffer.concat([iv, tag, ct]).toString("base64url");
}
export function open(sealed: string, key: Buffer): string | null {
try {
const buf = Buffer.from(sealed, "base64url");
const iv = buf.subarray(0, 12);
const tag = buf.subarray(12, 28);
const ct = buf.subarray(28);
const decipher = createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
} catch {
return null;
}
}
export function sha256(input: string): string {
return createHash("sha256").update(input).digest("base64url");
}
export function safeEqual(a: string, b: string): boolean {
const ba = Buffer.from(a);
const bb = Buffer.from(b);
if (ba.length !== bb.length) return false;
return timingSafeEqual(ba, bb);
}
export function randomToken(bytes = 32): string {
return randomBytes(bytes).toString("base64url");
}