Un esperimento su SQLCipher, gestione delle chiavi, recovery e verifica assistita dal modello AI, nato durante lo sviluppo di un'applicazione desktop locale.
Questo contenuto descrive un esperimento tecnico reale. Non è una certificazione di sicurezza né una guida universale alla progettazione crittografica.
L'obiettivo è mostrare, in modo trasparente e didattico, come è stato affrontato un problema reale e come il modello AI ha partecipato alle diverse fasi di analisi, sviluppo e verifica.
Prima di entrare nell'esperimento vero e proprio, un breve recap dei due modelli su cui si basa tutta la crittografia moderna. Non servono formule complesse: bastano le idee di fondo.
Nel modello simmetrico esiste una sola chiave, usata sia per cifrare sia per decifrare. In forma semplificata:
dove è il messaggio in chiaro, è il testo cifrato, è la chiave condivisa, e sono le funzioni di cifratura e decifratura.
Chi possiede K può fare entrambe le cose. È lo stesso principio usato da , solo con un algoritmo molto più robusto di quello del prossimo esempio.
Un esempio volutamente semplice, non sicuro, solo per vedere il meccanismo: ogni carattere del messaggio viene combinato con la chiave. La stessa chiave, applicata una seconda volta, riporta il messaggio originale.
Nota: questo è un XOR a scopo didattico, non un algoritmo sicuro. AES-256-GCM risolve le debolezze di uno schema così semplice (chiave troppo corta, pattern ripetuti, nessuna autenticazione) restando comunque un cifrario a chiave singola.
Nel modello asimmetrico esistono due chiavi diverse, generate insieme e legate matematicamente: una , che può essere distribuita a chiunque, e una , che deve restare segreta.
Chi cifra con la chiave pubblica non può decifrare il proprio stesso messaggio: serve la chiave privata corrispondente. È il principio dietro TLS/HTTPS, la firma digitale e la posta cifrata (PGP). Più avanti in questa pagina puoi provarlo con un esempio interattivo reale (RSA su numeri piccoli).
Un'applicazione di contabilità personale può conservare dati particolarmente sensibili: movimenti, saldi, categorie di spesa, pianificazioni e informazioni economiche. Il requisito di partenza era semplice da formulare:
La domanda dietro l'esperimento: come cifrare realmente un database SQLite locale senza trasformare l'applicazione in un sistema dipendente da server, account cloud o connessione Internet?
La soluzione implementata utilizza più livelli, ciascuno con una responsabilità diversa: , una master key casuale a 256 bit, , una Recovery Key separata e + per il recovery.
L'elemento più importante: non esiste una sola "password del database". Esiste una piccola architettura di gestione delle chiavi.
È facile pensare alla cifratura così: password → database cifrato. Ma resta una domanda: dove viene conservata la password? Se la chiave fosse salvata accanto al database in chiaro, la cifratura perderebbe gran parte del proprio valore.
La cifratura c'è, ma non protegge da un attaccante che ha accesso al filesystem: la chiave è lì, accanto al file.
Il problema si separa in due livelli indipendenti: cifrare i dati, e proteggere separatamente chi apre quella cifratura.
Ogni livello ha una responsabilità precisa e non conosce i dettagli implementativi degli altri.
Sequenza casuale di 32 byte (256 bit), generata con . Non deriva da nome utente, PC, Machine ID o password prevedibile.
Sostituisce il comportamento normale di SQLite con un database cifrato. Nel percorso di produzione la chiave viene applicata direttamente alla connessione tramite sqlite3_key, non tramite PRAGMA key. Segue un self-check con SELECT count(*) FROM sqlite_master;: se la chiave non è corretta, l'apertura fallisce.
La master key viene protetta con CryptProtectData, legato all'utente Windows corrente, ed evita di conservarla in chiaro nel filesystem. Per riaprire il database si usa CryptUnprotectData.
Non cifra il database: protegge la stessa master key, per il caso in cui DPAPI non sia più disponibile o serva accedere da un altro ambiente.
La Recovery Key diventa una chiave crittografica tramite (64 MiB, 3 iterazioni, parallelismo 1, salt 16 byte, output 32 byte). Quella chiave cifra la master key con , che offre anche autenticazione: rileva se il dato cifrato è stato alterato.
Estratti semplificati dal codice analizzato durante l'assessment, uno per ciascun passaggio dell'architettura a strati vista sopra. Sono riscritti per leggibilità didattica, non copiati riga per riga dal repository.
32 byte casuali dalla sorgente di casualità del sistema operativo, non da una libreria di terze parti.
use windows::Win32::Security::Cryptography::BCryptGenRandom;
let mut master_key = [0u8; 32];
BCryptGenRandom(None, &mut master_key, BCRYPT_USE_SYSTEM_PREFERRED_RNG)?;
// master_key ora contiene 256 bit di entropia del sistema operativo
La riga evidenziata è l'unico posto in cui la master key viene creata: da qui in poi viene solo protetta, mai rigenerata.
La chiave viene passata direttamente alla connessione, non tramite comando SQL — e viene subito verificata con un self-check.
ffi::sqlite3_key(
connection.handle(),
key.as_bytes().as_ptr().cast(),
key.as_bytes().len() as i32,
);
connection.query_row("SELECT count(*) FROM sqlite_master", [], |r| r.get::<_, i64>(0))?;
// se la chiave è sbagliata, questa query fallisce: è il self-check
Senza questo self-check, una chiave sbagliata aprirebbe comunque una connessione "valida" ma illeggibile — l'errore emergerebbe solo molto più tardi, in un punto meno chiaro del codice.
use windows::Win32::Security::Cryptography::CryptProtectData;
let protected_blob = CryptProtectData(&master_key_blob, None, None, None, None, flags)?;
fs::write(vault_path, &protected_blob)?;
// il blob è leggibile solo dall'utente Windows corrente
Da qui in poi il file su disco non contiene mai la master key in chiaro: contiene un blob che Windows sa aprire solo per lo stesso account che lo ha creato.
use argon2::{Argon2, Params};
let params = Params::new(64 * 1024, 3, 1, Some(32))?; // memoria KiB, iterazioni, parallelismo
let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, params);
let mut derived_key = [0u8; 32];
argon2.hash_password_into(recovery_key.as_bytes(), &salt, &mut derived_key)?;
I parametri evidenziati sono esattamente quelli che puoi muovere nel simulatore più sotto.
use aes_gcm::{Aes256Gcm, Nonce, aead::Aead};
let cipher = Aes256Gcm::new_from_slice(&derived_key)?;
let encrypted_master_key = cipher.encrypt(nonce, master_key.as_ref())?;
// encrypted_master_key include anche il tag di autenticazione GCM
Se qualcuno modifica anche un solo byte di encrypted_master_key, il tag di autenticazione GCM fa fallire la decifratura invece di restituire dati corrotti silenziosamente.
Questa sezione è un'incursione volutamente separata dall'esperimento sul database: non riguarda SQLCipher, ma il secondo grande modello di crittografia citato più sopra, per poterlo confrontare davvero invece che a parole.
Lo scenario classico per capirla: Alice vuole mandare un messaggio a Bob attraverso un canale che Trudy può intercettare. Bob non ha mai incontrato Alice prima, quindi non possono essersi già accordati su una chiave segreta condivisa — il problema che la crittografia simmetrica, da sola, non risolve. Il messaggio che proveremo a trasferire è semplicemente una piccola parola scritta da Alice; nella pratica reale sarebbe più spesso una chiave simmetrica da consegnare in modo sicuro, non un messaggio lungo.
Non è una sequenza arbitraria: ogni passaggio esiste per una ragione matematica precisa, ed è esattamente qui che si inserisce il generatore interattivo qui sotto, per far vedere anche cosa succede quando un passaggio viene violato.
Perché primi, e non due numeri qualsiasi? Perché tutto lo schema si regge su un compito facile in un verso e difficile nell'altro: moltiplicare due primi per ottenere n è immediato; tornare indietro da n ai due fattori originali (fattorizzare) è impraticabile se i numeri sono abbastanza grandi. Con fattori non primi questa asimmetria si indebolisce o smette del tutto di reggere il ragionamento dei passaggi successivi.
n diventa il modulo pubblico: la base su cui si svolge tutta l'aritmetica di cifratura e decifratura. Può essere conosciuto da chiunque — è la segretezza di p e q presi separatamente, non quella di n, a proteggere lo schema.
Prima la versione senza gergo: è semplicemente "quanti numeri, contati da 1 fino a n−1, non hanno nessun fattore in comune con n". Prova con un esempio piccolo prima di leggere la formula: scegli n qui sotto e guarda quali numeri restano verdi.
I numeri verdi sono quelli coprimi con n — nessun fattore in comune. Contali: con n = 15 = 3 × 5 sono esattamente 8, e (3−1)×(5−1) = 2×4 = 8. Non è una coincidenza: quando n è il prodotto di due primi distinti, questo conteggio si scompone sempre in quel prodotto — una proprietà dimostrata, non una regola empirica che capita di funzionare.
Perché ci serve: esiste un teorema (di Eulero) secondo cui elevare un numero coprimo con n alla potenza φ(n) dà sempre resto 1 dividendo per n — una specie di "giro completo" nell'aritmetica modulare. Applicata due volte, una in cifratura e una in decifratura, è esattamente questa identità a far tornare il messaggio originale. E calcolare φ(n) è facile se conosci p e q, ma resta difficile quanto fattorizzare n se conosci solo n: la stessa porta a senso unico vista al passo 1.
"Coprimo" vuol dire che e e φ(n) non condividono nessun fattore comune maggiore di 1 — il loro massimo comun divisore (MCD, o gcd) è 1. Serve perché al passo successivo dobbiamo trovare l'inverso moltiplicativo di e modulo φ(n): un inverso del genere esiste solo se i due numeri sono coprimi.
In pratica e viene spesso scelto piccolo e fisso — 65537 è un valore comune in RSA reale, qui usiamo 17 per restare su numeri piccoli — perché rende la cifratura più veloce; poi si verifica che sia davvero coprimo con il φ(n) ottenuto. Se non lo è, si prova un altro e: non è un valore che deve avere una forma speciale, deve solo superare questo controllo.
d è l'unico numero tale che e × d, diviso per φ(n), lascia resto 1 — in simboli, e × d ≡ 1 (mod φ(n)). Si calcola con l', una variante dell'algoritmo per trovare il MCD che restituisce anche i coefficienti per esprimerlo come combinazione di e e φ(n): da lì si ricava direttamente l'inverso.
È questo il passaggio che rende d calcolabile solo da chi conosce φ(n) — quindi solo da chi conosce p e q separatamente, non solo il loro prodotto n.
Esempio numerico completo con p = 61, q = 53:
Cosa viaggia dove: Bob pubblica (e, n) — chiunque, incluso Trudy, può vederli. (d, n) resta solo con Bob. Alice usa (e, n) per cifrare; solo (d, n) può decifrare.
Cambia p, q ed e qui sotto. Prova prima con due primi diversi da quelli dell'esempio (per esempio p = 7, q = 11), poi prova a rompere deliberatamente lo schema con p = 9, q = 11, e = 7 — 9 non è primo — e osserva che il messaggio decifrato non torna più leggibile, anche usando "la chiave giusta" generata da quei numeri.
Nota onesta: con alcune combinazioni di numeri non primi il messaggio può tornare leggibile lo stesso, per pura coincidenza aritmetica — non perché la regola "servono due primi" sia sbagliata, ma perché piccoli numeri creano occasionalmente casi particolari. Con numeri delle dimensioni usate da RSA reale questa coincidenza è irrilevante: è un effetto visibile solo perché qui i numeri sono volutamente piccoli.
n = 3233 · φ(n) = 3120 · gcd(e, φ(n)) = 1 ✓ · d = 2753 ✓
Se scegli p o q non primi, il generatore calcola comunque n e (p−1)(q−1) per farti vedere il risultato — ma quel numero non è la vera φ(n), quindi anche con la decifratura "giusta" il messaggio che torna indietro non sarà quello di partenza: è la dimostrazione pratica del passo 1. Se scegli numeri molto piccoli, anche con p e q primi il messaggio può degradarsi perché più caratteri finiscono per condividere lo stesso resto modulo n — un limite di questo schema-giocattolo con l'alfabeto ASCII, non di RSA in sé.
C'è una domanda che la matematica qui sopra non copre: quando Bob invia (e, n) ad Alice attraverso lo stesso canale insicuro, come fa Alice a essere sicura che quella chiave sia davvero di Bob e non di Trudy, che si è finta Bob e ha spedito la propria coppia al posto suo?
RSA da solo non risponde a questa domanda: garantisce che solo chi possiede d possa decifrare, ma non dice nulla su chi possieda quel d. Se Trudy riesce a far credere ad Alice che la sua chiave pubblica sia quella di Bob, Alice cifrerà un messaggio che Trudy può leggere tranquillamente — un attacco detto man-in-the-middle.
Nella pratica il problema si risolve legando la chiave pubblica a un'identità tramite una terza parte fidata (un certificato digitale, firmato da una Certificate Authority — è il meccanismo dietro il lucchetto HTTPS del browser), oppure verificando l'impronta della chiave attraverso un canale diverso e già fidato.
Perché SQLCipher non usa questo schema. Anche a parte il problema dell'autenticità, RSA cifra pochi byte alla volta ed è ordini di grandezza più lento di AES su grandi quantità di dati: cifrare un intero database riga per riga con RSA sarebbe impraticabile. Nei sistemi reali (TLS, PGP) la chiave pubblica/privata non cifra i dati stessi: cifra o scambia in modo sicuro una chiave simmetrica, che poi fa il lavoro pesante — lo stesso ruolo che, nella nostra architettura, è giocato da DPAPI e da Argon2id: proteggono la master key simmetrica, non la sostituiscono con una coppia di chiavi.
Una chiave sola. Velocissima anche su un intero database. Richiede solo un modo sicuro per custodire quella chiave (DPAPI, Recovery Key) — nessun canale, nessun problema di autenticità.
Due chiavi legate matematicamente. Utile per scambiare un segreto tra parti che non si conoscono già (TLS, firma digitale), ma solo se l'autenticità della chiave pubblica è garantita da qualcos'altro (un certificato). Troppo lenta per cifrare l'intero contenuto di un database locale.
Lo scenario Alice/Bob non è astratto: è letteralmente quello che succede ogni volta che apri il sito della tua banca. Il browser (Alice) deve mandare dati sensibili alla banca (Bob) attraverso una rete che considera insicura per definizione.
La proprietà di sicurezza chiave, detta con precisione: non è che chi cifra "non ha alcun segreto" — deve comunque generare la chiave di sessione senza farsela rubare prima di spedirla. La proprietà reale è che la chiave pubblica stessa non ha bisogno di restare segreta: la banca può stamparla su un cartellone, chiunque può vederla, e questo non aiuta nessuno a decifrare nulla. È l'opposto della crittografia simmetrica, dove chiunque ottenga la chiave può sia cifrare sia decifrare, e quindi entrambe le parti devono proteggerla allo stesso modo. Con la chiave pubblica, solo un lato — quello che detiene la chiave privata — deve proteggere un segreto.
Nessuna di queste minacce riguarda "la matematica di RSA in sé essere sbagliata" — riguardano tutte come le chiavi vengono generate, distribuite o usate.
Visto sopra: se l'autenticità della chiave pubblica non è verificata (certificato falso o non controllato), un intermediario può farsi passare per il destinatario legittimo.
Se n è troppo corto, un attaccante può risalire a p e q e ricostruirsi la chiave privata da solo. Quanto è pratico dipende interamente dalla dimensione di n — è il tema della prossima sottosezione.
Se p e q non sono scelti con un generatore di numeri casuali davvero imprevedibile, o sono troppo vicini fra loro, esistono scorciatoie matematiche (come il metodo di fattorizzazione di Fermat) molto più veloci della fattorizzazione generica.
Alcuni attacchi storici famosi (come quello di Bleichenbacher) non rompono la matematica di RSA: sfruttano errori nel modo in cui un'implementazione gestisce i messaggi cifrati male formati, per estrarre informazioni un bit alla volta.
Misurano effetti collaterali dell'hardware — tempo di calcolo, consumo energetico, emissioni elettromagnetiche — durante l'uso della chiave privata, per dedurne i bit senza mai fattorizzare nulla.
L'algoritmo di Shor, se eseguito su un computer quantistico abbastanza grande, fattorizzerebbe n in tempi brevi. Nessun computer quantistico oggi esistente si avvicina a questa capacità su chiavi di dimensione reale — è per questo che si sta già lavorando a schemi "post-quantistici" alternativi.
Il modo più semplice per rompere RSA partendo dalla sola chiave pubblica (e, n) è la divisione successiva: provare a dividere n per 2, poi per 3, poi per 5... finché uno dei tentativi non lascia resto zero. Il primo che funziona è p; da lì q = n / p si ottiene gratis. È anche il metodo più lento possibile: gli attacchi reali usano algoritmi molto più furbi — in particolare il crivello generale dei campi di numeri (GNFS) — ma restano comunque governati dalla stessa idea di fondo: il tempo esplode al crescere delle cifre di n.
Scegli un n e premi "Fattorizza".
Il tentativo si ferma da solo dopo alcuni milioni di divisioni o dopo qualche secondo, per non bloccare la pagina — è già abbastanza per sentire la differenza fra "istantaneo" e "già scomodo" restando su numeri piccolissimi.
Nessun attaccante userebbe davvero un solo processore su numeri di queste dimensioni. Si dividerebbe lo spazio dei possibili divisori fra molte macchine, ciascuna responsabile di una fetta — esattamente come è successo per davvero con RSA-768, distribuito su centinaia di macchine per quasi due anni. Qui puoi vederlo in scala minuscola: stesso principio, stesso tipo di n, ma la ricerca viene divisa fra più "macchine" simulate.
Scegli quante macchine simulare e premi "Avvia attacco distribuito".
Le macchine sono simulate a turno in un solo browser (non c'è vero multi-core qui), ma ognuna analizza solo la propria fetta dello spazio di ricerca — proprio come farebbero macchine reali indipendenti. Con 1 sola macchina questo n richiede un paio di secondi; prova a salire a 8 o 64 e guarda il tempo scendere.
Le chiavi RSA reali non usano moduli come questi: ne usano di centinaia di cifre decimali. Ecco cosa raccontano le fattorizzazioni realmente completate finora, con l'algoritmo più veloce oggi conosciuto — non la divisione successiva:
RSA-100 (100 cifre decimali): fattorizzato entro il 1° aprile 1991, nel giro di settimane con i mezzi dell'epoca.
RSA-155 (512 bit): la fattorizzazione richiese circa 4.000 anni-MIPS di calcolo distribuito, completata in alcuni mesi coordinando più centri di calcolo.
RSA-768 (768 bit, 232 cifre): il solo crivello, su un singolo processore dell'epoca, avrebbe richiesto circa millecinquecento anni; distribuito su centinaia di macchine, l'intero sforzo durò circa due anni e mezzo.
RSA-1024: gli stessi autori della fattorizzazione di RSA-768 hanno stimato che fattorizzare un modulo a 1024 bit sarebbe circa mille volte più difficile; a oggi nessuna fattorizzazione pubblica di un numero fra RSA-704 e RSA-2048 è mai stata completata.
RSA-2048, la dimensione oggi raccomandata: la difficoltà cresce di un fattore ancora enormemente più grande rispetto a RSA-1024 — ben oltre quanto i computer classici attuali possano affrontare in tempi utili.
Nota: i computer quantistici, se abbastanza potenti, cambierebbero questo quadro grazie all'algoritmo di Shor citato sopra tra le minacce — motivo per cui la ricerca su schemi "post-quantistici" è già attiva oggi, non in previsione di un problema lontanissimo nel tempo.
Argon2id trasforma la Recovery Key in una chiave crittografica facendola "costare" tempo e memoria: più costa a te aprire il database una volta, più costa a un attaccante provare miliardi di password. Muovi i controlli per vedere il compromesso — è una simulazione didattica, non una misura di sicurezza reale.
Nota: le curve usate qui sono semplificate a scopo didattico, per mostrare la direzione del compromesso (memoria e iterazioni più alte → apertura più lenta ma attacco più costoso). Non rappresentano un benchmark reale di Argon2id, né una stima calibrata su hardware d'attacco esistente.
Clicca sui passaggi per seguire cosa succede in ciascuno dei tre scenari.
L'applicazione esisteva già con un database SQLite non cifrato. Il processo non modifica brutalmente il file originale.
PRAGMA integrity_check.L'AI non è stata trattata come una funzione che "scrive codice e basta". La separazione dei ruoli riduce un rischio tipico dello sviluppo assistito dall'AI: chiedere allo stesso modello di implementare qualcosa e considerare automaticamente corretta la propria implementazione.
Aiuta a scomporre il problema — proteggere il database, proteggere la chiave, gestire recovery e migrazione — e a confrontare alternative architetturali.
Implementa modifiche circoscritte nel repository, in sprint limitati: analisi iniziale → piano → patch limitata → test → verifica.
Rilegge il codice in modalità read-only, senza modificarlo, e ricostruisce ciò che il codice fa realmente — distinguendo osservato da dedotto.
Trasforma il codice risultante in una spiegazione comprensibile, come questa stessa pagina.
Il controllo finale torna sempre al codice, ai test e alle evidenze — mai alla parola del modello su se stesso.
Questo esperimento ti è stato utile per capire il funzionamento?
Hai un'esperienza diversa da raccontare, un errore, un'obiezione o un'idea da aggiungere?
Invia un contributo →