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
This commit is contained in:
74
server/utils/crypto.mjs
Normal file
74
server/utils/crypto.mjs
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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}`)
|
||||
}
|
||||
Reference in New Issue
Block a user