← Torna al blog

Come RedaktPDF cifra i tuoi PDF: un'analisi approfondita della nostra architettura E2EE

Jury D'Ambros··8 min di lettura

La maggior parte degli editor PDF online elabora i tuoi file sui propri server. Ricevono il documento in chiaro, lo manipolano e lo rispediscono indietro. Nel momento in cui il file lascia il tuo browser, è leggibile dall'infrastruttura di quell'azienda — dai loro ingegneri, dai loro log e da chiunque ottenga accesso non autorizzato ai loro sistemi.

RedaktPDF funziona diversamente. Ogni PDF che carichi come utente registrato viene cifrato nel tuo browser prima che un solo byte raggiunga i nostri server. Il server memorizza testo cifrato che non può leggere. Questo articolo spiega esattamente come funziona — la gerarchia delle chiavi, le primitive crittografiche e le decisioni di progettazione dietro ogni scelta.

Per una panoramica non tecnica, vedi Perché la crittografia end-to-end è importante per l'editing PDF.

Il modello zero-knowledge

Il termine "zero-knowledge" ha molteplici significati in crittografia, ma in questo contesto significa qualcosa di specifico: il server detiene dati crittograficamente inutili senza informazioni che solo l'utente possiede.

Ecco cosa il server di RedaktPDF sa dei tuoi documenti cifrati:

  • La KEK incapsulata (cifrata) — inutile senza la tua master key
  • Le file key incapsulate (cifrate) — inutili senza la KEK
  • Blob cifrati in S3 — illeggibili senza le file key
  • Il salt PBKDF2 — non segreto; necessario per la derivazione delle chiavi
  • Il tuo indirizzo email e i metadati dell'account

Ecco cosa il server non vede mai:

  • La tua password (solo un hash bcrypt è memorizzato per l'autenticazione)
  • La master key — derivata nel tuo browser, mai trasmessa
  • La KEK in chiaro — decapsulata solo nel tuo browser
  • Le file key in chiaro — decapsulate solo nel tuo browser
  • Contenuto PDF, immagini delle pagine e testo estratto — cifrati prima del caricamento

Perché questo conta in pratica? Considera lo scenario peggiore possibile: un attaccante esfiltra l'intero database e ogni oggetto S3. Ha le chiavi incapsulate e i blob cifrati. Senza la tua password, non può derivare la master key. Senza la master key, la KEK incapsulata non può essere decifrata. Senza la KEK, le chiavi per file rimangono sigillate. La breccia non produce nulla di azionabile.

Questo non è un impegno contrattuale — è una proprietà crittografica dell'architettura.

La gerarchia delle chiavi

RedaktPDF usa una gerarchia di chiavi a tre livelli. Ogni livello serve a uno scopo distinto.

User Password
    |
    v PBKDF2(password, salt, 600,000 iterations, SHA-256)
Master Key (MK) ---- stored nowhere (derived on-demand)
    |
    v AES-GCM wrapKey
Key Encryption Key (KEK) ---- wrapped copy stored in DB
    |
    v AES-GCM wrapKey (per document)
File Key (FK) ---- wrapped copy stored in DB per document
    |
    v AES-256-GCM encrypt
Encrypted PDF, page images, text metadata ---- stored in S3

Master Key (MK): derivata dalla tua password usando PBKDF2. Non viene mai memorizzata da nessuna parte — né sul server né nel browser oltre la sessione corrente. Ogni volta che accedi a una sessione cifrata, la MK viene ri-derivata dalla tua password ed esiste solo in memoria. Se chiudi la scheda, scompare.

Key Encryption Key (KEK): una chiave AES-GCM a 256 bit generata casualmente. Viene generata una sola volta quando abiliti la crittografia e poi immediatamente incapsulata (cifrata) con la MK. La forma incapsulata è memorizzata nel database. La forma in chiaro esiste nella memoria del browser solo durante una sessione attiva.

La KEK esiste per un motivo operativo: i cambi di password. Quando cambi la tua password, una nuova MK viene derivata. Senza un livello KEK, la file key di ogni singolo documento dovrebbe essere ri-cifrata con la nuova MK. Con il livello KEK, solo il wrapper della KEK necessita di un aggiornamento — una singola scrittura nel database indipendentemente da quanti documenti hai. Tutte le file key esistenti rimangono valide.

File Key (FK): una chiave AES-GCM a 256 bit generata casualmente, unica per documento. Cifra i byte effettivi del PDF, le immagini delle pagine, le miniature e il testo estratto. La forma incapsulata è memorizzata nel database insieme ai metadati del documento. Isolare le chiavi per documento significa che un'ipotetica compromissione di chiave colpisce esattamente un documento, non l'intera libreria di file.

Derivazione delle chiavi: dalla password alla master key

La derivazione della master key usa l'implementazione PBKDF2 della Web Crypto API. Ecco il codice di produzione da packages/crypto/src/index.ts:

const PBKDF2_ITERATIONS = 600_000;

export async function deriveMasterKey(
  password: string,
  salt: Uint8Array,
): Promise<CryptoKey> {
  const keyMaterial = await globalThis.crypto.subtle.importKey(
    'raw',
    new TextEncoder().encode(password),
    'PBKDF2',
    false,
    ['deriveKey'],
  );

  return globalThis.crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt as unknown as ArrayBuffer,
      iterations: PBKDF2_ITERATIONS,
      hash: 'SHA-256',
    },
    keyMaterial,
    { name: 'AES-GCM', length: 256 },
    false, // not extractable
    ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'],
  );
}

Esaminiamo il codice:

  1. importKey('raw', ...) — importa la password codificata in UTF-8 come materiale di chiave grezzo. Non è ancora una chiave utilizzabile; sono solo byte alimentati alla funzione di derivazione.

  2. deriveKey(...) — applica PBKDF2 con SHA-256 e 600.000 iterazioni per produrre la master key.

  3. { name: 'AES-GCM', length: 256 } — specifica che l'output è una chiave AES-GCM a 256 bit.

  4. false (extractable) — questo è critico. La master key è non estraibile. Anche il codice JavaScript in esecuzione nello stesso contesto del browser non può leggere i byte grezzi della chiave. Esiste all'interno dell'archivio chiavi interno della Web Crypto API e può essere usata solo per operazioni crittografiche, mai esportata. Questo impedisce a uno script malevolo di leggere la chiave dalla memoria.

  5. ['encrypt', 'decrypt', 'wrapKey', 'unwrapKey'] — gli utilizzi della chiave. La master key può incapsulare e decapsulare la KEK e può cifrare/decifrare direttamente se necessario.

Perché 600.000 iterazioni? PBKDF2 è intenzionalmente lento — ogni iterazione aggiuntiva rende gli attacchi a forza bruta proporzionalmente più costosi. La raccomandazione OWASP del 2023 è di 600.000 iterazioni per PBKDF2-SHA256. Questo fa sì che un singolo tentativo di password richieda tempo CPU misurabile. A 600.000 iterazioni, un attaccante che tenti un attacco a dizionario contro un salt rubato deve pagare quel costo CPU per ogni password candidata. Anche con un cluster GPU, crackare una password forte è impraticabile.

Il salt casuale da 16 byte garantisce che password identiche producano master key diverse, sconfiggendo gli attacchi con rainbow table.

Cifratura di un PDF

Una volta disponibile una file key, tutta la cifratura dei dati usa la stessa funzione. Ecco l'encryptBlob di produzione dal pacchetto crypto:

export async function encryptBlob(
  data: Uint8Array,
  key: CryptoKey,
): Promise<Uint8Array> {
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
  const ciphertext = await globalThis.crypto.subtle.encrypt(
    { name: 'AES-GCM', iv },
    key,
    data as unknown as ArrayBuffer,
  );
  return concat(iv, new Uint8Array(ciphertext));
}

Tre cose da notare:

IV casuale per ogni cifratura. IV_BYTES è 12 (byte), la lunghezza IV raccomandata dal NIST per AES-GCM. Un nuovo IV da 12 byte viene generato per ogni chiamata a encryptBlob. Questo significa che cifrare lo stesso PDF due volte produce testo cifrato diverso. Se gli IV venissero riutilizzati con la stessa chiave, la sicurezza di AES-GCM collasserebbe — due testi cifrati con la stessa chiave e IV rivelano lo XOR dei testi in chiaro e permettono a un attaccante di falsificare i tag di autenticazione. Gli IV casuali eliminano questo rischio.

Cifratura autenticata. AES-GCM è un cifrario AEAD (Authenticated Encryption with Associated Data). Fornisce sia confidenzialità (il contenuto non può essere letto) sia integrità (il contenuto non può essere modificato silenziosamente). Il tag di autenticazione GCM accodato al testo cifrato rileva qualsiasi manomissione. Se un blob cifrato memorizzato viene modificato in S3, la decifratura lancerà un errore invece di restituire testo in chiaro corrotto.

Formato auto-descrittivo. L'IV è anteposto al testo cifrato (concat(iv, new Uint8Array(ciphertext))). L'output è un singolo array di byte: [12 byte IV][byte rimanenti: testo cifrato + tag di autenticazione di 16 byte]. La decifratura legge i primi 12 byte come IV, il resto come testo cifrato autenticato. Non è necessaria una memorizzazione separata dell'IV.

Questa stessa funzione cifra i byte del file PDF, ogni immagine di pagina renderizzata, le immagini delle miniature e il JSON del testo estratto — tutto ciò che potrebbe esporre il contenuto del documento.

Quando usi l'editor PDF sicuro per applicare modifiche, queste operazioni vengono assemblate ed esportate interamente nel tuo browser. Il documento modificato non viene mai ricaricato in forma decifrata.

Il flusso di upload

Ecco la sequenza per caricare un PDF cifrato:

  1. Il tuo browser legge il file PDF come Uint8Array.
  2. PDF.js renderizza ogni pagina localmente a 150 DPI (risoluzione piena) e 72 DPI (miniatura). Nessun rendering avviene lato server.
  3. PDF.js estrae le posizioni del testo localmente per supportare ricerca e selezione.
  4. Il browser genera una File Key (FK) casuale.
  5. La FK cifra: i byte del PDF, ogni PNG di pagina renderizzata, ogni PNG miniatura e il JSON delle posizioni del testo — tutto tramite encryptBlob.
  6. La FK viene incapsulata con la KEK dell'utente usando AES-GCM wrapKey.
  7. Il browser carica il PDF cifrato su S3 direttamente tramite un URL pre-firmato — il server emette l'URL ma non riceve mai il contenuto del file.
  8. Il browser invia i blob di pagina cifrati e la FK incapsulata a /api/documents/confirm-encrypted.
  9. Il server memorizza i blob cifrati in S3 e la FK incapsulata nel database. In nessun momento vede il PDF in chiaro, le immagini delle pagine o la file key.

Anche le operazioni di redazione seguono questo modello: le redazioni vengono applicate lato client alla rappresentazione PDF in-browser prima che il documento venga cifrato e caricato.

Perché AES-GCM per il key wrapping

Una nota sull'implementazione del key wrapping che differisce dalla tabella di documentazione in docs/SECURITY.md.

La tabella di SECURITY.md elenca "AES-KW (Key Wrap)" per la primitiva di key wrapping — un riferimento a RFC 3394 AES Key Wrap. L'implementazione effettiva usa wrapKey con AES-GCM. Ecco la funzione wrapKek:

export async function wrapKek(
  kek: CryptoKey,
  masterKey: CryptoKey,
): Promise<string> {
  const iv = globalThis.crypto.getRandomValues(new Uint8Array(IV_BYTES));
  const wrapped = await globalThis.crypto.subtle.wrapKey(
    'raw',
    kek,
    masterKey,
    {
      name: 'AES-GCM',
      iv,
    },
  );
  return toBase64(concat(iv, new Uint8Array(wrapped)));
}

La distinzione conta: AES-KW (RFC 3394) non usa un IV. Fornisce integrità tramite un valore di controllo dell'integrità incorporato, ma il suo meccanismo di autenticazione è più ristretto di quello di GCM. AES-GCM con un IV casuale fornisce AEAD completo — confidenzialità, integrità e autenticazione con un tag di autenticazione a 128 bit. Qualsiasi modifica a una chiave incapsulata è rilevabile.

Usare AES-GCM in tutto lo stack (sia per la cifratura dei dati sia per il key wrapping) ha un altro beneficio pratico: la coerenza. L'implementazione usa una singola primitiva con proprietà ben comprese ed eccellente supporto da parte del browser. Non c'è il sovraccarico cognitivo di ragionare su due modalità crittografiche diverse.

La tabella di SECURITY.md è una lacuna nella documentazione; l'implementazione effettiva è più robusta. Questo articolo documenta ciò che il codice fa realmente.

Cosa significa in pratica

Mettiamolo insieme con uno scenario concreto. Supponiamo che il database di RedaktPDF e tutti gli oggetti S3 fossero esfiltrati domani. L'attaccante ottiene:

  • Il tuo indirizzo email
  • Un hash bcrypt della tua password (fattore di costo 12 — non utilizzabile direttamente)
  • Il salt PBKDF2
  • La KEK incapsulata
  • Le FK incapsulate per ciascuno dei tuoi documenti
  • I blob cifrati per ogni documento

Per decifrare un singolo documento, l'attaccante deve:

  1. Crackare la tua password dall'hash bcrypt (o indovinarla dal salt PBKDF2, cosa che non velocizza significativamente la ricerca della password)
  2. Usare la password crackata + salt per eseguire PBKDF2 a 600.000 iterazioni per derivare la master key
  3. Usare la master key per decapsulare la KEK
  4. Usare la KEK per decapsulare la FK del documento target
  5. Usare la FK per decifrare i blob cifrati

Ogni passo richiede il completamento riuscito del passo precedente. Il costo di PBKDF2 è progettato per rendere il passo 2 un tempo di calcolo significativo per ogni tentativo, rendendo impraticabili gli attacchi a forza bruta contro password forti.

Una breccia completa espone i metadati ma non il contenuto. Questa è la garanzia pratica dell'architettura zero-knowledge. Per uno sguardo più ampio sulle pratiche di privacy nell'editing PDF, vedi la nostra guida alla privacy PDF.

Letture ulteriori

Le specifiche di Web Crypto API, PBKDF2 e AES-GCM dietro a questa implementazione:

Pronto a provare RedaktPDF?

Modifica, oscura e annota PDF direttamente dal browser — gratis e cifrato.

Inizia ora

Strumenti correlati

Articoli correlati