import { Credential } from '@nodecfdi/credentials/node'; import { writeFile, mkdir } from 'fs/promises'; import { join } from 'path'; import { prisma } from '../config/database.js'; import { env } from '../config/env.js'; import { encryptFielCredentials, encrypt, decryptFielCredentials } from './sat/sat-crypto.service.js'; import { emailService } from './email/email.service.js'; import type { FielStatus } from '@horux/shared'; /** * Sube y valida credenciales FIEL */ export async function uploadFiel( tenantId: string, cerBase64: string, keyBase64: string, password: string ): Promise<{ success: boolean; message: string; status?: FielStatus }> { try { // Decodificar archivos de Base64 const cerData = Buffer.from(cerBase64, 'base64'); const keyData = Buffer.from(keyBase64, 'base64'); // Validar que los archivos sean válidos y coincidan let credential: Credential; try { credential = Credential.create( cerData.toString('binary'), keyData.toString('binary'), password ); } catch (error: any) { return { success: false, message: 'Los archivos de la FIEL no son válidos o la contraseña es incorrecta', }; } // Verificar que sea una FIEL (no CSD) if (!credential.isFiel()) { return { success: false, message: 'El certificado proporcionado no es una FIEL (e.firma). Parece ser un CSD.', }; } // Obtener información del certificado const certificate = credential.certificate(); const rfc = certificate.rfc(); const serialNumber = certificate.serialNumber().bytes(); // validFromDateTime() y validToDateTime() retornan strings ISO o objetos DateTime const validFromRaw = certificate.validFromDateTime(); const validUntilRaw = certificate.validToDateTime(); const validFrom = new Date(String(validFromRaw)); const validUntil = new Date(String(validUntilRaw)); // Verificar que no esté vencida if (new Date() > validUntil) { return { success: false, message: 'La FIEL está vencida desde ' + validUntil.toLocaleDateString(), }; } // Encriptar credenciales (per-component IV/tag) const { encryptedCer, encryptedKey, encryptedPassword, cerIv, cerTag, keyIv, keyTag, passwordIv, passwordTag, } = encryptFielCredentials(cerData, keyData, password); // Guardar o actualizar en BD await prisma.fielCredential.upsert({ where: { tenantId }, create: { tenantId, rfc, cerData: encryptedCer, keyData: encryptedKey, keyPasswordEncrypted: encryptedPassword, cerIv, cerTag, keyIv, keyTag, passwordIv, passwordTag, serialNumber, validFrom, validUntil, isActive: true, }, update: { rfc, cerData: encryptedCer, keyData: encryptedKey, keyPasswordEncrypted: encryptedPassword, cerIv, cerTag, keyIv, keyTag, passwordIv, passwordTag, serialNumber, validFrom, validUntil, isActive: true, updatedAt: new Date(), }, }); // Save encrypted files to filesystem (dual storage) try { const fielDir = join(env.FIEL_STORAGE_PATH, rfc.toUpperCase()); await mkdir(fielDir, { recursive: true, mode: 0o700 }); // Re-encrypt for filesystem (independent keys from DB) const fsEncrypted = encryptFielCredentials(cerData, keyData, password); await writeFile(join(fielDir, 'certificate.cer.enc'), fsEncrypted.encryptedCer, { mode: 0o600 }); await writeFile(join(fielDir, 'certificate.cer.iv'), fsEncrypted.cerIv, { mode: 0o600 }); await writeFile(join(fielDir, 'certificate.cer.tag'), fsEncrypted.cerTag, { mode: 0o600 }); await writeFile(join(fielDir, 'private_key.key.enc'), fsEncrypted.encryptedKey, { mode: 0o600 }); await writeFile(join(fielDir, 'private_key.key.iv'), fsEncrypted.keyIv, { mode: 0o600 }); await writeFile(join(fielDir, 'private_key.key.tag'), fsEncrypted.keyTag, { mode: 0o600 }); // Encrypt and store metadata const metadata = JSON.stringify({ serial: serialNumber, validFrom: validFrom.toISOString(), validUntil: validUntil.toISOString(), uploadedAt: new Date().toISOString(), rfc: rfc.toUpperCase(), }); const metaEncrypted = encrypt(Buffer.from(metadata, 'utf-8')); await writeFile(join(fielDir, 'metadata.json.enc'), metaEncrypted.encrypted, { mode: 0o600 }); await writeFile(join(fielDir, 'metadata.json.iv'), metaEncrypted.iv, { mode: 0o600 }); await writeFile(join(fielDir, 'metadata.json.tag'), metaEncrypted.tag, { mode: 0o600 }); } catch (fsError) { console.error('[FIEL] Filesystem storage failed (DB storage OK):', fsError); } // Notify admin that client uploaded FIEL const tenant = await prisma.tenant.findUnique({ where: { id: tenantId }, select: { nombre: true, rfc: true }, }); if (tenant) { emailService.sendFielNotification({ clienteNombre: tenant.nombre, clienteRfc: tenant.rfc, }).catch(err => console.error('[EMAIL] FIEL notification failed:', err)); } const daysUntilExpiration = Math.ceil( (validUntil.getTime() - Date.now()) / (1000 * 60 * 60 * 24) ); return { success: true, message: 'FIEL configurada correctamente', status: { configured: true, rfc, serialNumber, validFrom: validFrom.toISOString(), validUntil: validUntil.toISOString(), isExpired: false, daysUntilExpiration, }, }; } catch (error: any) { console.error('[FIEL Upload Error]', error); return { success: false, message: error.message || 'Error al procesar la FIEL', }; } } /** * Obtiene el estado de la FIEL de un tenant */ export async function getFielStatus(tenantId: string): Promise { const fiel = await prisma.fielCredential.findUnique({ where: { tenantId }, select: { rfc: true, serialNumber: true, validFrom: true, validUntil: true, isActive: true, }, }); if (!fiel || !fiel.isActive) { return { configured: false }; } const now = new Date(); const isExpired = now > fiel.validUntil; const daysUntilExpiration = Math.ceil( (fiel.validUntil.getTime() - now.getTime()) / (1000 * 60 * 60 * 24) ); return { configured: true, rfc: fiel.rfc, serialNumber: fiel.serialNumber || undefined, validFrom: fiel.validFrom.toISOString(), validUntil: fiel.validUntil.toISOString(), isExpired, daysUntilExpiration: isExpired ? 0 : daysUntilExpiration, }; } /** * Elimina la FIEL de un tenant */ export async function deleteFiel(tenantId: string): Promise { try { await prisma.fielCredential.delete({ where: { tenantId }, }); return true; } catch { return false; } } /** * Obtiene las credenciales desencriptadas para usar en sincronización * Solo debe usarse internamente por el servicio de SAT */ export async function getDecryptedFiel(tenantId: string): Promise<{ cerContent: string; keyContent: string; password: string; rfc: string; } | null> { const fiel = await prisma.fielCredential.findUnique({ where: { tenantId }, }); if (!fiel || !fiel.isActive) { return null; } // Verificar que no esté vencida if (new Date() > fiel.validUntil) { return null; } try { // Desencriptar credenciales (per-component IV/tag) const { cerData, keyData, password } = decryptFielCredentials( Buffer.from(fiel.cerData), Buffer.from(fiel.keyData), Buffer.from(fiel.keyPasswordEncrypted), Buffer.from(fiel.cerIv), Buffer.from(fiel.cerTag), Buffer.from(fiel.keyIv), Buffer.from(fiel.keyTag), Buffer.from(fiel.passwordIv), Buffer.from(fiel.passwordTag) ); return { cerContent: cerData.toString('binary'), keyContent: keyData.toString('binary'), password, rfc: fiel.rfc, }; } catch (error) { console.error('[FIEL Decrypt Error]', error); return null; } } /** * Verifica si un tenant tiene FIEL configurada y válida */ export async function hasFielConfigured(tenantId: string): Promise { const status = await getFielStatus(tenantId); return status.configured && !status.isExpired; }