mirror of
https://github.com/R0m1k3/LogiFlow.git
synced 2026-10-12 01:37:40 +02:00
Encryption (server/crypto.ts): - AES-256-GCM with a key derived from ENCRYPTION_KEY, falling back to SESSION_SECRET so existing deployments need no new configuration - stored format enc:v1:<iv>:<tag>:<data>; encryption is idempotent and decryption passes legacy plaintext through unchanged, so nothing breaks mid-migration - tampered data or a changed key raises an explicit error instead of returning garbage - encrypt/decrypt is confined to the storage layer: group writes encrypt smtpPassword (decrypted only in emailService at connection time, never sent to the client), NocoDB config writes encrypt apiToken and reads decrypt it so the invoice verification and the admin page behave as before - startup migration sweep encrypts secrets already stored in plaintext, idempotently; the active-config log line no longer prints the token Mail history (supplier_mail_logs): - every send attempt is recorded: delivery, store, supplier, recipient, subject, status sent/failed with error, message id, user id and name; a logging failure never fails the send itself - GET /api/supplier-mail-logs restricted to the user's stores (admin may filter by store) - on the reconciliation page the mail icon turns green once a request has been sent, with the date and sender in the tooltip; clicking again resends - table created in init.sql, versioned migrations and the production startup migration, with delivery/group indexes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHdE9nEh8zHhQNGpCgrtYe
77 lines
2.9 KiB
TypeScript
77 lines
2.9 KiB
TypeScript
import crypto from 'crypto';
|
|
|
|
/**
|
|
* Chiffrement au repos des secrets applicatifs (mot de passe SMTP des
|
|
* magasins, jeton API NocoDB).
|
|
*
|
|
* - AES-256-GCM (chiffrement authentifié : toute altération en base est détectée)
|
|
* - Clé dérivée de ENCRYPTION_KEY, avec repli sur SESSION_SECRET pour que la
|
|
* production existante fonctionne sans nouvelle variable d'environnement
|
|
* - Format stocké : enc:v1:<iv>:<tag>:<données>, tout en base64
|
|
* - Les valeurs héritées en clair sont acceptées en lecture (déchiffrement
|
|
* transparent) et re-chiffrées par le balayage de démarrage
|
|
*
|
|
* ATTENTION : changer ENCRYPTION_KEY/SESSION_SECRET après coup rend les
|
|
* secrets déjà chiffrés illisibles — il faudrait alors les ressaisir.
|
|
*/
|
|
|
|
const PREFIX = 'enc:v1:';
|
|
|
|
function getKey(): Buffer {
|
|
const secret = process.env.ENCRYPTION_KEY || process.env.SESSION_SECRET;
|
|
if (!secret) {
|
|
// Même repli figé que les sessions : mieux vaut un chiffrement à clé
|
|
// faible qu'un stockage en clair, et la production définit toujours
|
|
// SESSION_SECRET via docker-compose.
|
|
return crypto.createHash('sha256').update('logiflow-fallback-secret-key').digest();
|
|
}
|
|
return crypto.createHash('sha256').update(secret).digest();
|
|
}
|
|
|
|
export function isEncryptedSecret(value: string | null | undefined): boolean {
|
|
return typeof value === 'string' && value.startsWith(PREFIX);
|
|
}
|
|
|
|
/**
|
|
* Chiffre un secret. Les valeurs vides et les valeurs déjà chiffrées sont
|
|
* renvoyées telles quelles (idempotent : pas de double chiffrement possible).
|
|
*/
|
|
export function encryptSecret(value: string | null | undefined): string | null | undefined {
|
|
if (!value || isEncryptedSecret(value)) {
|
|
return value;
|
|
}
|
|
|
|
const iv = crypto.randomBytes(12);
|
|
const cipher = crypto.createCipheriv('aes-256-gcm', getKey(), iv);
|
|
const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
|
|
const tag = cipher.getAuthTag();
|
|
|
|
return `${PREFIX}${iv.toString('base64')}:${tag.toString('base64')}:${encrypted.toString('base64')}`;
|
|
}
|
|
|
|
/**
|
|
* Déchiffre un secret. Une valeur non chiffrée (héritage d'avant le
|
|
* chiffrement) est renvoyée telle quelle.
|
|
*/
|
|
export function decryptSecret(value: string | null | undefined): string | null | undefined {
|
|
if (!value || !isEncryptedSecret(value)) {
|
|
return value;
|
|
}
|
|
|
|
const parts = value.slice(PREFIX.length).split(':');
|
|
if (parts.length !== 3) {
|
|
throw new Error('Secret chiffré illisible : format inattendu');
|
|
}
|
|
|
|
try {
|
|
const [iv, tag, data] = parts.map(p => Buffer.from(p, 'base64'));
|
|
const decipher = crypto.createDecipheriv('aes-256-gcm', getKey(), iv);
|
|
decipher.setAuthTag(tag);
|
|
return Buffer.concat([decipher.update(data), decipher.final()]).toString('utf8');
|
|
} catch {
|
|
throw new Error(
|
|
'Secret chiffré illisible : clé de chiffrement changée ou donnée altérée. Ressaisissez la valeur.'
|
|
);
|
|
}
|
|
}
|