- 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
75 lines
2.3 KiB
JavaScript
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}`)
|
|
}
|