Files
PANEL_BASES_ANEXO24/src/lib/server/crypto-core.ts
hreyes b8be307892 feature/integracion-cloudrestore-targets (#9)
integraciones para cloud recovery

Reviewed-on: #9
Co-authored-by: hreyes <hreyes@aduanasoft.com.mx>
Co-committed-by: hreyes <hreyes@aduanasoft.com.mx>
2026-06-09 14:58:48 +00:00

87 lines
3.3 KiB
TypeScript

/**
* Lógica pura de cifrado AES-256-GCM (estándar Aduanasoft §4). Sin dependencias de
* SvelteKit ni de entorno, para ser testeable de forma aislada. La clave de 256 bits
* se inyecta como parámetro; quien la resuelve desde el entorno es `crypto.ts`.
*
* Formato del sobre: `gcm:<iv_b64>:<tag_b64>:<ciphertext_b64>`.
*/
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
export const ALGORITHM = 'aes-256-gcm';
export const IV_LENGTH = 12; // 96 bits, recomendado para GCM
export const KEY_LENGTH = 32; // 256 bits
export const VERSION_PREFIX = 'gcm';
/** Cifra `plaintext` con la clave dada y devuelve el sobre versionado. */
export function encryptWithKey(plaintext: string, key: Buffer): string {
if (key.length !== KEY_LENGTH) {
throw new Error(`La clave debe ser de ${KEY_LENGTH} bytes; se recibieron ${key.length}.`);
}
const iv = randomBytes(IV_LENGTH);
const cipher = createCipheriv(ALGORITHM, key, iv);
const ciphertext = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return [
VERSION_PREFIX,
iv.toString('base64'),
authTag.toString('base64'),
ciphertext.toString('base64')
].join(':');
}
/**
* Descifra un sobre producido por `encryptWithKey`. Lanza si el formato es inválido
* o si la autenticación GCM falla (dato manipulado o clave incorrecta).
*/
export function decryptWithKey(payload: string, key: Buffer): string {
if (key.length !== KEY_LENGTH) {
throw new Error(`La clave debe ser de ${KEY_LENGTH} bytes; se recibieron ${key.length}.`);
}
const parts = String(payload ?? '').split(':');
if (parts.length !== 4 || parts[0] !== VERSION_PREFIX) {
throw new Error('Formato de texto cifrado inválido (se esperaba gcm:iv:tag:ciphertext).');
}
const iv = Buffer.from(parts[1], 'base64');
const authTag = Buffer.from(parts[2], 'base64');
const ciphertext = Buffer.from(parts[3], 'base64');
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString('utf8');
}
/** Indica si un valor ya está cifrado con este esquema (para detectar datos legados en plano). */
export function isEncrypted(value: string | null | undefined): boolean {
return typeof value === 'string' && value.startsWith(`${VERSION_PREFIX}:`);
}
/**
* Decodifica una clave de 32 bytes desde una cadena base64 o hex.
* Lanza con mensaje claro si no decodifica a la longitud esperada.
*/
export function decodeKey(raw: string | undefined): Buffer {
if (!raw || !raw.trim()) {
throw new Error(
'ENCRYPTION_KEY no está configurada. Genera una con: ' +
'node -e "console.log(require(\'crypto\').randomBytes(32).toString(\'base64\'))"'
);
}
const trimmed = raw.trim();
let key = Buffer.from(trimmed, 'base64');
if (key.length !== KEY_LENGTH) {
key = Buffer.from(trimmed, 'hex');
}
if (key.length !== KEY_LENGTH) {
throw new Error(
`ENCRYPTION_KEY debe decodificar a ${KEY_LENGTH} bytes (256 bits) en base64 o hex; ` +
`se obtuvieron ${key.length} bytes.`
);
}
return key;
}