Files
kreios/vite-plugins/encrypt-models.js
T
carmine.savino 1208187a17 feat: integrate Draco compression for 3D models in backoffice
- Added support for uploading and compressing GLB models using Draco in the backoffice.
- Implemented commands for importing and re-compressing 3D models.
- Updated model resource to handle 3D model uploads and validations.
- Introduced a new script for compressing GLB files with Draco.
- Modified Vite configuration to remove GLB files from the build output.
- Updated package dependencies to include GLTF-transform libraries for handling 3D models.
- Added tests to ensure proper functionality of 3D model handling and compression.
2026-11-01 10:35:50 +01:00

121 lines
5.4 KiB
JavaScript

// NON ATTIVO e da rivedere: i modelli 3D ora sono caricati dal backoffice (App\Support\Configuratore\Modelli3d)
// e non passano più dalla build; per attivare la cifratura va portata lì, sulle copie Draco.
// Nota storica: non è registrato in vite.config.js. Per usarlo va aggiunto ai plugin dopo dracoModels
// (facendogli cifrare i GLB già compressi in <outDir>/assets/models) e i caricamenti in starter.js vanno
// fatti con loadModello() di src/js/model_loader.js, impostando sul GLTFLoader anche il DRACOLoader.
//
// Cifra i modelli GLB in fase di build, così nella cartella pubblica non esiste nessun .glb in chiaro.
//
// Ogni src/assets/models/<percorso>.glb diventa <outDir>/assets/m/<hash>.bin, dove:
// - hash = sha256(chiave || percorso) troncato: l'URL non rivela il nome del modello;
// - contenuto = IV (12 byte) + AES-256-GCM(glb), con il percorso come dato autenticato.
// La chiave arriva da MODELS_KEY (64 caratteri esadecimali, in .env / .env.production) ed è iniettata
// nel bundle offuscata; il browser la ricostruisce e decifra con WebCrypto (src/js/model_loader.js).
//
// È una protezione contro il download diretto (tab Network, estensioni "scarica modelli 3D"), non una
// protezione assoluta: chi legge il JS e fa debug della pagina può comunque recuperare la geometria.
import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import path from 'node:path';
import { loadEnv } from 'vite';
const OUT_SUBDIR = 'assets/m';
async function listGlb(dir, base = dir) {
const out = [];
for (const entry of await fs.readdir(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) out.push(...(await listGlb(full, base)));
else if (entry.name.toLowerCase().endsWith('.glb')) out.push(path.relative(base, full).split(path.sep).join('/'));
}
return out;
}
// Elimina gli eventuali .glb copiati in chiaro da vite-plugin-static-copy (es. file archiviati in assets/data).
async function removePlainGlb(dir) {
let entries;
try {
entries = await fs.readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) await removePlainGlb(full);
else if (entry.name.toLowerCase().endsWith('.glb')) await fs.rm(full);
}
}
export function modelFileName(key, relPath) {
return crypto.createHash('sha256').update(key).update(relPath, 'utf8').digest('hex').slice(0, 32) + '.bin';
}
function encrypt(key, relPath, data) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
cipher.setAAD(Buffer.from(relPath, 'utf8'));
const body = Buffer.concat([cipher.update(data), cipher.final()]);
// Formato WebCrypto: ciphertext seguito dal tag di 16 byte.
return Buffer.concat([iv, body, cipher.getAuthTag()]);
}
export default function encryptModels({ modelsDir }) {
let key;
let outDir;
return {
name: 'kreios-encrypt-models',
apply: 'build',
config(userConfig, { mode }) {
const envDir = userConfig.envDir ?? process.cwd();
const hex = (loadEnv(mode, envDir, '').MODELS_KEY ?? process.env.MODELS_KEY ?? '').trim();
if (/^[0-9a-fA-F]{64}$/.test(hex)) {
key = Buffer.from(hex, 'hex');
} else {
if (hex) throw new Error('MODELS_KEY deve essere di 64 caratteri esadecimali (32 byte).');
// Senza chiave fissa ogni build ne genera una nuova: funziona, ma cambia tutti gli URL
// dei modelli e i browser li riscaricano a ogni deploy.
key = crypto.randomBytes(32);
console.warn('[encrypt-models] MODELS_KEY non impostata: uso una chiave casuale per questa build.');
}
// La chiave nel bundle è mascherata (XOR) per non comparire come costante leggibile.
const mask = crypto.randomBytes(32);
const masked = Buffer.from(key.map((b, i) => b ^ mask[i]));
return {
define: {
__KREIOS_MK__: JSON.stringify([...masked]),
__KREIOS_MM__: JSON.stringify([...mask]),
},
};
},
configResolved(config) {
outDir = config.build.outDir;
},
// sequential: parte dopo il writeBundle di vite-plugin-static-copy.
writeBundle: {
sequential: true,
order: 'post',
async handler() {
await removePlainGlb(path.join(outDir, 'assets'));
const target = path.join(outDir, OUT_SUBDIR);
await fs.mkdir(target, { recursive: true });
const files = await listGlb(modelsDir);
for (const rel of files) {
const src = path.join(modelsDir, rel);
const dest = path.join(target, modelFileName(key, rel));
// In watch mode non ricifra i modelli che non sono cambiati.
const [s, d] = await Promise.all([fs.stat(src), fs.stat(dest).catch(() => null)]);
if (d && d.mtimeMs >= s.mtimeMs) continue;
await fs.writeFile(dest, encrypt(key, rel, await fs.readFile(src)));
}
console.log(`[encrypt-models] ${files.length} modelli cifrati in ${OUT_SUBDIR}/`);
},
},
};
}