feat(erinnerungen): mailserver in der app einrichten
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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ebdd6c313a
commit
01bbb3ba9a
@@ -0,0 +1,41 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user