Checkpoint: Fenêtre administrateur Salaires finalisée : import multipart limité, chiffrement AES-256-GCM avant stockage persistant hors conteneur, métadonnées/index métier minimisés en base, consultation PDF déchiffrée côté serveur avec no-store, filtres année/mois et comparaison M-1 expliquée. Réindexation sûre des archives partielles sans écrasement du PDF. Validation réelle sur deux liasses mensuelles : index partiel signalé sans invention de données, comparaison affichée et lecture après redémarrage. 55 tests Vitest, TypeScript et build de production validés.
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 1m54s
Some checks failed
Validation applicative / TypeScript, tests et build (push) Failing after 1m54s
This commit is contained in:
62
server/payrollCrypto.ts
Normal file
62
server/payrollCrypto.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const ALGORITHM = "aes-256-gcm";
|
||||
const IV_BYTES = 12;
|
||||
const AUTH_TAG_BYTES = 16;
|
||||
|
||||
export type EncryptedPayrollFile = {
|
||||
ciphertext: Buffer;
|
||||
ivBase64: string;
|
||||
authTagBase64: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Lit la clé dédiée aux bulletins. La clé ne transite jamais vers le client et
|
||||
* doit être une valeur Base64 représentant précisément 32 octets AES-256.
|
||||
*/
|
||||
function getPayrollEncryptionKey(): Buffer {
|
||||
const encodedKey = process.env.PAYROLL_ENCRYPTION_KEY?.trim();
|
||||
if (!encodedKey) throw new Error("Clé de chiffrement des bulletins absente");
|
||||
|
||||
const key = Buffer.from(encodedKey, "base64");
|
||||
if (key.length !== 32) {
|
||||
throw new Error("La clé de chiffrement des bulletins doit contenir 32 octets");
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
/** Sondage sans divulgation de la clé, destiné aux contrôles administrateur. */
|
||||
export function getPayrollEncryptionStatus() {
|
||||
try {
|
||||
getPayrollEncryptionKey();
|
||||
return { ready: true as const, algorithm: "AES-256-GCM" };
|
||||
} catch {
|
||||
return { ready: false as const, algorithm: "AES-256-GCM" };
|
||||
}
|
||||
}
|
||||
|
||||
/** Chiffre un PDF avant son envoi vers le stockage persistant. */
|
||||
export function encryptPayrollPdf(plaintext: Buffer): EncryptedPayrollFile {
|
||||
const key = getPayrollEncryptionKey();
|
||||
const iv = crypto.randomBytes(IV_BYTES);
|
||||
const cipher = crypto.createCipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_BYTES });
|
||||
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
||||
return {
|
||||
ciphertext,
|
||||
ivBase64: iv.toString("base64"),
|
||||
authTagBase64: cipher.getAuthTag().toString("base64"),
|
||||
};
|
||||
}
|
||||
|
||||
/** Déchiffre le document uniquement côté serveur, après contrôle des droits. */
|
||||
export function decryptPayrollPdf(encrypted: EncryptedPayrollFile): Buffer {
|
||||
const key = getPayrollEncryptionKey();
|
||||
const iv = Buffer.from(encrypted.ivBase64, "base64");
|
||||
const authTag = Buffer.from(encrypted.authTagBase64, "base64");
|
||||
if (iv.length !== IV_BYTES || authTag.length !== AUTH_TAG_BYTES) {
|
||||
throw new Error("Paramètres de chiffrement de bulletin invalides");
|
||||
}
|
||||
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_BYTES });
|
||||
decipher.setAuthTag(authTag);
|
||||
return Buffer.concat([decipher.update(encrypted.ciphertext), decipher.final()]);
|
||||
}
|
||||
Reference in New Issue
Block a user