SMTP-Server, Port, Verschluesselung, Zugang und Absender unter Mehr -> Erinnerungen, gespeichert in der neuen Tabelle app_setting. Das Passwort liegt mit AES-256-GCM verschluesselt (Schluessel aus BETTER_AUTH_SECRET), wird nie angezeigt und nicht exportiert. Die .env hat weiter Vorrang. Der Zeitplan laeuft jetzt immer und prueft je Durchlauf, ob ein Server da ist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
42 lines
1.6 KiB
TypeScript
42 lines
1.6 KiB
TypeScript
import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto';
|
|
|
|
/**
|
|
* Encrypts secrets that settings must keep, such as the SMTP password.
|
|
*
|
|
* AES-256-GCM with a key derived from BETTER_AUTH_SECRET: a copy of the
|
|
* database alone does not reveal the password. Changing BETTER_AUTH_SECRET
|
|
* makes stored secrets unreadable - `openSecret` then returns null and the
|
|
* password has to be entered again. Pure apart from node:crypto, so tested.
|
|
*/
|
|
|
|
const VERSION = 'v1';
|
|
|
|
function deriveKey(masterSecret: string): Buffer {
|
|
return Buffer.from(hkdfSync('sha256', masterSecret, 'bricktrack', 'app-setting-secret', 32));
|
|
}
|
|
|
|
export function sealSecret(plain: string, masterSecret: string): string {
|
|
const iv = randomBytes(12);
|
|
const cipher = createCipheriv('aes-256-gcm', deriveKey(masterSecret), iv);
|
|
const encrypted = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]);
|
|
const tag = cipher.getAuthTag();
|
|
return [VERSION, iv.toString('base64'), tag.toString('base64'), encrypted.toString('base64')].join(':');
|
|
}
|
|
|
|
export function openSecret(sealed: string, masterSecret: string): string | null {
|
|
const [version, iv, tag, encrypted] = sealed.split(':');
|
|
if (version !== VERSION || !iv || !tag || encrypted === undefined) return null;
|
|
|
|
try {
|
|
const decipher = createDecipheriv('aes-256-gcm', deriveKey(masterSecret), Buffer.from(iv, 'base64'));
|
|
decipher.setAuthTag(Buffer.from(tag, 'base64'));
|
|
return Buffer.concat([
|
|
decipher.update(Buffer.from(encrypted, 'base64')),
|
|
decipher.final(),
|
|
]).toString('utf8');
|
|
} catch {
|
|
// Wrong key or tampered value: treat as "no password stored".
|
|
return null;
|
|
}
|
|
}
|