SMTP per-shop configurabile dalle Impostazioni (password cifrata AES-256-GCM)
- Settings: smtpHost/Port/User/Pass(cifrata)/Secure/From. Vuoto -> provider default dell'app (env); compilato -> invio dal SMTP del merchant. - crypto.server: encrypt/decrypt AES-256-GCM (chiave APP_ENCRYPTION_KEY). - mailer: SmtpConfig + buildTransport(smtp) per-shop o env; mailFrom override. Sia ricevuta cliente sia notifica merchant usano lo SMTP del merchant. - proxy: costruisce SmtpConfig (decifra pass), passa ai due invii. - admin: nuovo tab 'Email (SMTP)'; password mai ri-mostrata (vuoto = invariata). - Migrazione smtp_settings. APP_ENCRYPTION_KEY: dev in .env, prod = fly secret. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Mv83a29B4eFv5ixoj6PoE1
This commit is contained in:
47
app/app/lib/crypto.server.ts
Normal file
47
app/app/lib/crypto.server.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Cifratura simmetrica per segreti a riposo (es. password SMTP per-shop).
|
||||
* AES-256-GCM. Chiave da env APP_ENCRYPTION_KEY (qualsiasi lunghezza: derivata
|
||||
* a 32 byte via SHA-256). In prod = `fly secrets set APP_ENCRYPTION_KEY=...`.
|
||||
*/
|
||||
import {
|
||||
createCipheriv,
|
||||
createDecipheriv,
|
||||
createHash,
|
||||
randomBytes,
|
||||
} from "node:crypto";
|
||||
|
||||
const PREFIX = "enc:v1:";
|
||||
|
||||
function key(): Buffer {
|
||||
const raw = process.env.APP_ENCRYPTION_KEY;
|
||||
if (!raw || raw.length < 16) {
|
||||
throw new Error("APP_ENCRYPTION_KEY mancante o troppo corta (>=16 char)");
|
||||
}
|
||||
return createHash("sha256").update(raw).digest();
|
||||
}
|
||||
|
||||
/** Cifra -> "enc:v1:<iv>:<tag>:<data>" (base64). */
|
||||
export function encryptSecret(plain: string): string {
|
||||
const iv = randomBytes(12);
|
||||
const cipher = createCipheriv("aes-256-gcm", key(), iv);
|
||||
const enc = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
||||
const tag = cipher.getAuthTag();
|
||||
return `${PREFIX}${iv.toString("base64")}:${tag.toString("base64")}:${enc.toString("base64")}`;
|
||||
}
|
||||
|
||||
/** Decifra; se non e' nel formato cifrato, ritorna il valore invariato. */
|
||||
export function decryptSecret(value: string): string {
|
||||
if (!value.startsWith(PREFIX)) return value;
|
||||
const parts = value.split(":");
|
||||
if (parts.length !== 5) return "";
|
||||
const iv = Buffer.from(parts[2]!, "base64");
|
||||
const tag = Buffer.from(parts[3]!, "base64");
|
||||
const data = Buffer.from(parts[4]!, "base64");
|
||||
const decipher = createDecipheriv("aes-256-gcm", key(), iv);
|
||||
decipher.setAuthTag(tag);
|
||||
return Buffer.concat([decipher.update(data), decipher.final()]).toString(
|
||||
"utf8",
|
||||
);
|
||||
}
|
||||
|
||||
export const SECRET_PREFIX = PREFIX;
|
||||
Reference in New Issue
Block a user