// NON ATTIVO: non è registrato in vite.config.js. Per usarlo va aggiunto ai plugin dopo dracoModels // (facendogli cifrare i GLB già compressi in /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/.glb diventa /assets/m/.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}/`); }, }, }; }