Files
Emailsorter/server/utils/crypto.mjs
ANDJ ecae89a79d fix(dev): Vite-API-Proxy, Auth, Stripe-Mails und Backend-Erweiterungen
- Client: API-Basis-URL (joinApiUrl, /v1-Falle), Vite strictPort + Proxy 127.0.0.1, Nicht-JSON-Fehler

- Server: /api-404 ohne Wildcard-Bug, SPA-Fallback, Auth-Middleware, Cron, Mailer, Crypto

- Routen: OAuth-State, Email/Stripe/Analytics; client/.env.example

Made-with: Cursor
2026-04-03 00:23:01 +02:00

75 lines
2.3 KiB
JavaScript

/**
* AES-256-GCM for IMAP passwords. ENCRYPTION_KEY = 64 hex chars (32 bytes).
* Legacy: if decrypt fails or key missing, value treated as plaintext.
*/
import crypto from 'crypto'
const ALGO = 'aes-256-gcm'
const IV_LEN = 16
const AUTH_TAG_LEN = 16
function getKeyBuffer() {
const hex = process.env.ENCRYPTION_KEY || ''
if (hex.length !== 64) {
throw new Error('ENCRYPTION_KEY must be 64 hex characters (32 bytes). Generate: openssl rand -hex 32')
}
return Buffer.from(hex, 'hex')
}
export function encrypt(text) {
if (text == null || text === '') return ''
const key = getKeyBuffer()
const iv = crypto.randomBytes(IV_LEN)
const cipher = crypto.createCipheriv(ALGO, key, iv, { authTagLength: AUTH_TAG_LEN })
const enc = Buffer.concat([cipher.update(String(text), 'utf8'), cipher.final()])
const authTag = cipher.getAuthTag()
const combined = Buffer.concat([iv, authTag, enc])
return combined.toString('base64url')
}
export function decrypt(encoded) {
if (!encoded) return ''
const buf = Buffer.from(String(encoded), 'base64url')
if (buf.length < IV_LEN + AUTH_TAG_LEN + 1) {
throw new Error('invalid ciphertext')
}
const key = getKeyBuffer()
const iv = buf.subarray(0, IV_LEN)
const authTag = buf.subarray(IV_LEN, IV_LEN + AUTH_TAG_LEN)
const data = buf.subarray(IV_LEN + AUTH_TAG_LEN)
const decipher = crypto.createDecipheriv(ALGO, key, iv, { authTagLength: AUTH_TAG_LEN })
decipher.setAuthTag(authTag)
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8')
}
/** Encrypt IMAP password when ENCRYPTION_KEY is set; otherwise store plaintext. */
export function encryptImapSecret(plain) {
if (plain == null || plain === '') return ''
if (!process.env.ENCRYPTION_KEY) return String(plain)
try {
return encrypt(plain)
} catch (e) {
logWarnOnce('encryptImapSecret', e.message)
return String(plain)
}
}
/** Decrypt IMAP secret; on failure return as plaintext (legacy). */
export function decryptImapSecret(stored) {
if (stored == null || stored === '') return ''
if (!process.env.ENCRYPTION_KEY) return String(stored)
try {
return decrypt(stored)
} catch {
return String(stored)
}
}
let warnedEncrypt = false
function logWarnOnce(tag, msg) {
if (warnedEncrypt) return
warnedEncrypt = true
console.warn(`[crypto] ${tag}: ${msg}`)
}