// services/cryptoUtil.js // Symmetric encryption for secrets we must be able to READ back (unlike // portal passwords, which are one-way hashed) — specifically each user's SAP // Service-Layer password, which has to be replayed to SAP at login time. // AES-256-GCM with a key derived from JWT_SECRET. Output is a self-describing // string "v1:::" (all base64), so decrypt needs no extra // state. NOT for passwords you only ever compare — use hashing for those. 'use strict'; const crypto = require('crypto'); function key() { const secret = process.env.JWT_SECRET || 'sap-portal-secret'; return crypto.createHash('sha256').update('sapcred:' + secret).digest(); // 32 bytes } function encrypt(plain) { if (plain == null || plain === '') return ''; const iv = crypto.randomBytes(12); const cipher = crypto.createCipheriv('aes-256-gcm', key(), iv); const enc = Buffer.concat([cipher.update(String(plain), 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); return `v1:${iv.toString('base64')}:${tag.toString('base64')}:${enc.toString('base64')}`; } function decrypt(blob) { if (!blob) return ''; try { const parts = String(blob).split(':'); if (parts.length !== 4 || parts[0] !== 'v1') return ''; const iv = Buffer.from(parts[1], 'base64'); const tag = Buffer.from(parts[2], 'base64'); const data = Buffer.from(parts[3], 'base64'); const decipher = crypto.createDecipheriv('aes-256-gcm', key(), iv); decipher.setAuthTag(tag); return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8'); } catch { return ''; } } module.exports = { encrypt, decrypt };