Vai al contenuto
HexaTransfer
Torna al blog
Approfondimenti tecnici

Costruisci an Crittografato Condivisione file App from Scratch

Step-by-step tutorial to build an encrypted file sharing application. Frontend encryption, secure backend, e deployment walkthrough.

Costruire un'app di condivisione file cifrata significa mettere la crittografia nel browser, non nel server. Il Garante per la protezione dei dati personali ha specificato che quando il titolare del trattamento non può accedere ai dati in chiaro, la valutazione del rischio ex articolo 32 GDPR risulta significativamente semplificata. Uno stack minimale si presenta così: un frontend React alimentato da Vite che cifra con AES-256-GCM tramite la Web Crypto API, un backend Fastify che tratta ogni upload come un blob opaco, storage compatibile S3 (Cloudflare R2 o Backblaze B2) e un breve URL di condivisione dove la chiave di decifratura vive dopo il # così non raggiunge mai il server. Si può spedire una versione funzionante in circa 400 righe di codice e ospitarla per meno di $5 al mese.

Decisioni Architetturali che Contano Davvero

La decisione singola più importante è dove vive la chiave di cifratura. Se tocca mai il tuo server, non hai crittografia end-to-end, hai cifratura lato server con passaggi extra. Il pattern corretto: generare una chiave casuale a 256 bit nel browser, cifrare il file con essa, caricare il ciphertext e mettere la chiave nel frammento URL (https://tuaapp.com/f/abc123#k=base64key). I browser non inviano mai i frammenti nelle richieste HTTP, così la chiave rimane lato client.

Seconda scelta: upload a chunk. Per file superiori a 100 MB serve l'upload multiparte riprendibile, altrimenti qualsiasi Wi-Fi instabile uccide il trasferimento. L'API multiparte di S3 supporta chunk minimi da 5 MB e fino a 10.000 parti, dandoti un limite di 50 GB. Pianificarlo dal primo giorno.

Impostare lo Scheletro del Progetto

Iniziare con due pacchetti: un frontend Vite + React e un backend Fastify. Il frontend gestisce tutta la crittografia, il backend gestisce storage e metadati. Usare TypeScript per la type safety su ArrayBuffer e CryptoKey.

pnpm create vite@latest frontend -- --template react-ts
pnpm create fastify backend

Aggiungere queste dipendenze al backend: @fastify/multipart, @aws-sdk/client-s3, @aws-sdk/s3-request-presigner e better-sqlite3 per i metadati di condivisione. Mantenere lo schema SQLite minuscolo: shares(id, object_key, size_bytes, expires_at, download_count, max_downloads). Nessun nome file, nessun dato utente, nessun IP.

Scrivere la Pipeline di Cifratura Frontend

Generare una chiave, cifrare con AES-256-GCM e produrre un blob di ciphertext più una chiave base64 per il frammento URL:

async function encryptFile(file: File) {
  const key = await crypto.subtle.generateKey(
    { name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']
  );
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const plaintext = await file.arrayBuffer();
  const ciphertext = await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv }, key, plaintext
  );
  const rawKey = await crypto.subtle.exportKey('raw', key);
  const blob = new Blob([iv, new Uint8Array(ciphertext)]);
  return { blob, keyBase64: toBase64(rawKey) };
}

Per file superiori a 50 MB, sostituire questo con una versione in streaming che cifra chunk da 4 MB e li aggiunge a un ReadableStream. L'heap di Safari mobile si blocca su tutto ciò che è più grande di circa 400 MB in un singolo ArrayBuffer.

Progettare l'Endpoint di Upload

Il backend non dovrebbe sapere nulla di utile. Accettare un POST con il ciphertext, generare un ID URL-safe casuale a 16 caratteri, memorizzarlo in SQLite con scadenza e fare streaming del body direttamente su S3:

fastify.post('/upload', async (req, reply) => {
  const id = nanoid(16);
  const key = `blobs/${id}`;
  const upload = new Upload({
    client: s3,
    params: { Bucket: 'hexa-transfers', Key: key, Body: req.raw }
  });
  await upload.done();
  db.prepare('INSERT INTO shares VALUES (?, ?, ?, ?, 0, ?)').run(
    id, key, req.headers['content-length'], Date.now() + 7*86400*1000, 10
  );
  return { id };
});

Scadenza predefinita a 7 giorni, massimo 10 download. Servono default aggressivi perché l'alternativa è una crescita illimitata dello storage. Usare un cron per eliminare i blob scaduti di notte.

Generare Link di Condivisione con Chiavi nel Frammento

Una volta che l'upload finisce, costruire l'URL di condivisione lato client:

const { id } = await uploadResponse.json();
const shareUrl = `${location.origin}/f/${id}#k=${keyBase64}`;

Quel frammento non lascia mai il browser dell'utente. Quando un destinatario clicca il link, la tua app React legge window.location.hash, analizza la chiave, recupera il ciphertext e decifra localmente. Il server non ha alcun percorso verso il plaintext a breve di spingere JavaScript malevolo all'utente, il che è esattamente perché dovresti spedire una Content Security Policy stretta e pinnare gli hash delle subrisorse.

Implementare il Flusso di Download

Sulla pagina di download, recuperare il ciphertext come stream e decifrare allineato ai chunk:

const keyRaw = base64ToBytes(location.hash.slice(3));
const key = await crypto.subtle.importKey(
  'raw', keyRaw, { name: 'AES-GCM' }, false, ['decrypt']
);
const resp = await fetch(`/blob/${id}`);
const iv = new Uint8Array(await resp.body.getReader().read().then(r => r.value.slice(0, 12)));
const ct = await resp.arrayBuffer();
const pt = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ct.slice(12));
const url = URL.createObjectURL(new Blob([pt]));

Per i file grandi, usare StreamSaver.js o la File System Access API così i byte decifrati vanno su disco direttamente invece che in RAM.

Distribuire il Tutto

Frontend: spingere su Cloudflare Pages o Vercel, entrambi i free tier lo gestiscono. Backend: un singolo droplet DigitalOcean da $5 che esegue Node 22 dietro Caddy (TLS 1.3 automatico). Storage: Cloudflare R2 offre zero spese di egress, che è la killer feature per la condivisione file su scala. WeTransfer, Smash e SwissTransfer bruciano fortune sull'egress AWS; R2 cambia completamente l'economia.

Impostare header stretti tramite Caddy: Strict-Transport-Security, Content-Security-Policy: default-src 'self'; script-src 'self', X-Content-Type-Options: nosniff e Referrer-Policy: no-referrer. Questi chiudono i vettori comuni per la perdita del frammento URL tramite referrer o script iniettati.

Rendere l'App Conforme al GDPR per Default

Poiché il server vede solo ciphertext, si ha quasi nulla che si qualifichi come dato personale ai sensi dell'articolo 4 GDPR. Tuttavia, documentare le relazioni con i processori (R2 e DigitalOcean), impostare una retention massima dei blob di 30 giorni e pubblicare un chiaro avviso che i link di condivisione contengono chiavi e dovrebbero essere trasmessi su canali fidati dal mittente. Aggiungere rate limiting a 50 upload per IP per ora per scoraggiare gli abusi senza registrare le identità degli utenti. Seguire anche le indicazioni dell'ACN sulla sicurezza delle applicazioni web: CSP, HSTS e validazione degli input sono tutti requisiti.

Cosa Aggiungere una Volta che l'MVP Funziona

HexaTransfer è costruito essenzialmente su questa architettura: Web Crypto nel browser, storage di blob opachi, chiavi nei frammenti URL, zero plaintext sui server. Una volta che il flusso di base è attivo, le aggiunte di maggior valore sono: protezione con password sopra la chiave del frammento (PBKDF2 con 600.000 iterazioni), notifiche email per download per download tramite una casella di posta temporanea e segnalazione degli abusi che permette di flaggare l'hash del ciphertext senza rivelare il contenuto. Evitare di aggiungere account a meno che non ci sia una ragione specifica: trasformano la tua app da uno strumento di privacy in una responsabilità per i dati in una notte.

Prova su https://hexatransfer.com — gratuito, senza account, massimo 10 GB.

Invia file di grandi dimensioni in modo sicuro con crittografia end-to-end

Trasferisci file fino a 10 GB gratuitamente con crittografia end-to-end. Nessun account necessario. I tuoi file vengono crittografati nel browser prima del caricamento — nessun altro può leggerli.

Invia un file