Come può un server archiviare e sincronizzare i nostri segreti senza possedere le chiavi per leggerli? Seguiremo come Confidex crea le sue chiavi, le protegge in un lucchetto e le utilizza per crittografare i dati e verificare le modifiche.
Serratura
Solo l'Eletto può aprire la porta
Matrix Reloaded
Immagina di costruire un vault virtuale per archiviare dati segreti. Deve conservare i dati su un laptop dopo la chiusura dell'applicazione e consentirci di aprirli su un altro dispositivo. Il caveau avrà bisogno delle proprie chiavi. Dobbiamo memorizzarli in modo che la copia dei file non dia a un estraneo la possibilità di leggere o alterare il loro contenuto.
Un lock protegge le chiavi: un piccolo file crittografato. Per aprirlo sono necessari una password principale e un codice segreto, secret_key; nessuno dei due è nel file stesso. Potremmo stampare l'intera serratura sul giornale del mattino. I lettori ne riceverebbero una copia, ma la pubblicazione non rivelerebbe le chiavi private.
Supponiamo che un estraneo riceva questo file e venga a conoscenza della password principale. Ora possono ripetere ogni calcolo che richieda solo la password, ma quei calcoli non produrranno la chiave che apre la serratura: anche secret_key è un input. Il file non contiene alcun controllo separato della sola password. Per aprirlo è ancora necessario il codice segreto sconosciuto.
Quel codice proviene da dati crittograficamente casuali; una persona non lo inventa. Finché secret_key è sconosciuto, anche una password principale indovinata non apre la serratura: all'aggressore manca ancora un segreto casuale. Una password debole da sola non trasforma quindi la copia sul server in un pratico attacco al dizionario.
Se anche l'aggressore ottiene secret_key, può verificare le ipotesi della password principale offline. Questo è costoso, ma l'eventuale difficoltà dipende dalla forza della password.
Chiavi
Signori, cosa fanno le chiavi? Le chiavi... sbloccano le cose?
Pirati dei Caraibi – La maledizione del forziere fantasma
L'applicazione ottiene 32 byte casuali dal generatore crittografico del sistema operativo e li prepara per essere utilizzati come chiave. Questi byte preparati sono la chiave principale privata, master_key.
L'applicazione deriva sempre le stesse chiavi da master_key: ripetere gli stessi calcoli sugli stessi byte dà lo stesso risultato. Un laptop e un telefono possono quindi recuperare chiavi identiche. Per ottenere master_key su un nuovo dispositivo, l'applicazione deve prima aprire la serratura con la password principale e secret_key.
La prima coppia: crittografia
Io sono il maestro delle chiavi.
Acchiappafantasmi
Per archiviare i dati crittografati, l'applicazione calcola una chiave pubblica da master_key: secret_key. Può crittografare i dati per il vault, mentre la decrittografia richiede master_key.
“Pubblico” descrive il ruolo di questa chiave nell’algoritmo, non l’obbligo di darla a tutti. In Confidex rimane con l'utente. È anche il secondo segreto necessario per aprire la serratura, quindi rimane locale e non viene inviato in rete. Di per sé, secret_key consente a qualcuno di crittografare i dati per il vault, ma non di decrittografarli o di firmare modifiche per suo conto.
X25519 è l'algoritmo che prende master_key e calcola secret_key. Recuperare master_key da secret_key è praticamente impossibile. Un attacco classico contro un supercomputer immaginario che esegue un miliardo di miliardi di operazioni sulla curva al secondo richiederebbe in media circa 2,3 × 10¹² anni (stima SafeCurves).
La seconda coppia: firme
L'unica cosa originale è la firma
Prendimi se puoi
La prima coppia aiuta a nascondere il contenuto. Ora abbiamo bisogno di un modo per confermare che il proprietario del vault abbia autorizzato una modifica. Anche il server deve essere in grado di controllare le firme, quindi dobbiamo inviargli una chiave di verifica pubblica. La parte pubblica della prima coppia è secret_key, che il server non deve ricevere. Creiamo quindi una seconda coppia di chiavi permanente per le firme.
Invece di archiviare un altro segreto indipendente, ricaviamo una chiave di firma privata da master_key e la chiamiamo signing_key. L'applicazione lo utilizza per firmare i dati e il lucchetto. Altri dispositivi e il server utilizzano la sua parte pubblica per verificare le firme senza leggerne il contenuto.
Possiamo recuperare questa coppia dallo stesso master_key. L'applicazione ne deriva un valore iniziale: 32 byte che determinano in modo univoco la chiave di firma.
Per ricavare chiavi diverse per lavori diversi, includiamo un'etichetta fissa nel calcolo: qui, confidex/vault/ed25519/v1. Lo stesso master_key e la stessa etichetta producono sempre la stessa chiave.
La password principale non ha inserito questi calcoli. Entrambe le coppie dipendono dai byte casuali di master_key, non da una stringa inventata da una persona. Questa distinzione avrà importanza in seguito: la password proteggerà l'accesso alle chiavi già esistenti, quindi potremo cambiarla senza cambiare le chiavi stesse.
La derivazione utilizza una funzione di derivazione della chiave basata su hash: HKDF, HMAC-based Extract-and-Expand Key Derivation Function. Confidex lo usa con SHA-256, da qui il nome HKDF-SHA256. I 32 byte risultanti sono seed per la coppia Ed25519. La parte pubblica di signing_key non rivela secret_key, quindi possiamo inviarla al server per la verifica della firma.
Cos'è un file di blocco e cosa fa?
Apri le porte del vano capsule, HAL.
Un'Odissea nello spazio
Mi dispiace, Dave. Temo di non poterlo fare.
Il lucchetto memorizza un master_key crittografato. Per aprirlo sono necessari la password principale e secret_key. L'intestazione pubblica contiene la versione del formato, l'identificatore del vault, la parte pubblica di signing_key, un salt e i parametri di elaborazione della password. La versione in formato interno è crittografata insieme a master_key.
Quando si crea il blocco, l'applicazione ha già master_key, quindi calcola secret_key prima della crittografia. All'apertura, l'utente fornisce secret_key separatamente: è assente dalla copia pubblica del lucchetto.
Dopo la decrittografia, l'applicazione calcola la parte pubblica di master_key utilizzando X25519 e richiede che corrisponda a secret_key. Quindi recupera signing_key e confronta la sua parte pubblica con la chiave nell'intestazione. In questo modo la serratura si autocontrolla.
Per rallentare l'identificazione della password principale, ogni tentativo deve costare tempo e memoria. Confidex utilizza Argon2id con 64 MiB di memoria e tre passaggi. Anche l'elaborazione della password utilizza un sale casuale. Il sale non è segreto: fa sì che la stessa password produca risultati diversi in serrature diverse.
Se secret_key è già noto, la lunghezza della password cambia drasticamente la portata di un attacco. Esistono circa 12 milioni di combinazioni di 5 lettere latine minuscole casuali, circa 8 miliardi di 7 e circa 9,5 × 10¹⁶ di 12. Passare da 7 a 12 lettere aumenta lo spazio di ricerca di quasi 12 milioni di volte. Se la ricerca di 7 lettere richiedesse in media un giorno su una determinata macchina, 5 richiederebbero circa due minuti e 12 circa 33.000 anni.
Per ottenere una chiave dal risultato Argon2id e da secret_key, utilizziamo la funzione HKDF-SHA256 introdotta in precedenza, con l'etichetta confidex/lock/wrap/v1. Il risultato è wrapping_key, la chiave che crittografa il lucchetto.
Dobbiamo nascondere master_key e proteggerlo da manomissioni. XChaCha20-Poly1305 lo crittografa con wrapping_key e aggiunge un tag di autenticazione. Il tag lega il testo cifrato all'intestazione pubblica: una password errata o una modifica ai byte protetti ne impediscono l'apertura. Il tag e un valore casuale monouso, nonce, vengono archiviati con il testo cifrato.
Ogni volta che il blocco viene creato o crittografato nuovamente, l'applicazione ottiene un nuovo salt e nonce. I contenuti crittografati includono un byte di versione e i 32 byte di master_key; accanto ad essi vengono memorizzati un nonce da 24 byte e un tag da 16 byte. La documentazione dell'algoritmo descrive la costruzione.
Una mancata corrispondenza tra le chiavi recuperate può derivare da un blocco deliberatamente malformato o da un bug nel programma che lo ha creato. Il danneggiamento casuale dei byte viene solitamente rilevato prima, quando il tag viene controllato.
Il server non dispone dei segreti necessari per controllare questo tag. Per trasferire il lucchetto l'applicazione aggiunge quindi una firma realizzata con signing_key. Copre l'intero file finito, inclusa l'intestazione pubblica e i contenuti crittografati. Il server controlla la firma e, al primo caricamento, blocca l'intera parte pubblica di signing_key e l'identificatore del vault interno. Controlla le modifiche successive rispetto a questa chiave. Il lucchetto e i dati crittografati sono firmati con lo stesso signing_key, recuperato da master_key.
La volta
Ora possiamo pubblicare il lucchetto. Solo qualcuno che conosce secret_key e master password può leggere master_key, mentre chiunque può prendere la chiave di firma pubblica dal file di blocco e controllare i dati firmati senza vedere il contenuto del blocco o dei dati.
Considera una password, SuperPa55worD. Per ora vogliamo solo salvarlo: dopo aver chiuso l'applicazione, dovremmo essere in grado di inviare il file di blocco e la password crittografata ovunque senza che nessuno veda SuperPa55worD, consentendo comunque di verificare la nostra paternità. La crittografia deve essere reversibile quando abbiamo secret_key e master password. Un codice simmetrico può crittografare e decrittografare con la stessa chiave. Poiché non memorizziamo quella chiave, dobbiamo essere in grado di recuperarla utilizzando la serratura. Lo chiameremo message_key.
Otterremo un message_key separato per ogni nuova crittografia. Dobbiamo ancora provvedere al suo recupero: lasciare la chiave aperta accanto al testo cifrato consentirebbe a chiunque ne abbia una copia di leggere tutto.
Da nuovi byte casuali, l'applicazione crea una coppia temporanea X25519 solo per questa crittografia: record_key. Quindi prende la parte privata di record_key e la chiave pubblica permanente del vault, secret_key, e calcola un segreto condiviso. Una funzione separata deriva message_key da quel segreto; questa chiave crittografa la nostra password.
Durante la lettura, l'applicazione prende un'altra strada. Utilizza master_key e la parte pubblica di record_key. Queste due chiavi producono lo stesso segreto condiviso. Matematicamente, entrambi i percorsi moltiplicano lo stesso punto iniziale sulla curva per due numeri privati, in un ordine diverso. L'ordine non cambia il risultato. Possiamo quindi recuperare la chiave di decrittazione senza conservare la parte privata di record_key.
Ottenere questo segreto dalle due chiavi pubbliche è praticamente impossibile: entrambi i percorsi necessitano della corrispondente chiave privata. I punti pubblici non permettono di recuperare efficacemente i numeri privati che li hanno prodotti. Questo trucco matematico è alla base della sicurezza del caveau. Ma ciò non impedisce a qualcuno di darci un caveau diverso con la nostra serratura attaccata.
Ora possiamo memorizzare il testo cifrato insieme alla parte pubblica di record_key. La parte privata di record_key e quella utilizzata di message_key non sono più necessarie per la lettura futura. Successivamente, l'applicazione recupera la chiave di decrittografia da master_key e la parte pubblica archiviata di record_key. Un segreto permanente del vault può quindi decrittografare molti valori crittografati separatamente, ciascuno con il proprio message_key recuperato quando necessario.
Nascondere il testo non basta: bisogna anche individuare eventuali manomissioni durante la lettura. La crittografia aggiunge quindi un tag di autenticazione, un breve valore controllato insieme ai dati crittografati. Se il controllo fallisce, la domanda respinge il risultato. Non deve visualizzare testo casuale o alterato come password decrittografata con successo.
Abbiamo descritto una costruzione in cui una coppia asimmetrica aiuta a stabilire un segreto condiviso, mentre message_key ne crittografa il contenuto. Questo si chiama Hybrid Public Key Encryption o HPKE ed è specificato in RFC 9180. Confidex utilizza X25519 per concordare il segreto, HKDF-SHA256 per derivare le chiavi e ChaCha20-Poly1305 per la crittografia con controllo dell'integrità. Qui, HKDF prende il segreto condiviso concordato utilizzando record_key e la coppia master_key; questo è un lavoro separato dalla derivazione di Ed25519 seed. RFC 5869 descrive HKDF.
Per la crittografia successiva, l'applicazione crea un nuovo contesto record_key e un nuovo contesto HPKE. Riutilizzare una combinazione di message_key e nonce interrompe la protezione del codice. In Confidex, la chiave e nonce vengono derivate all'interno di HPKE, utilizzate per un messaggio, quindi il contesto viene scartato.
Lo stesso ChaCha20 utilizza un message_key a 256 bit. Per avere un’idea di un attacco diretto, immagina un supercomputer immaginario che testa un miliardo di miliardi di candidati al secondo. La ricerca delle 2²⁵⁶ chiavi possibili richiederebbe circa 1,8 × 10⁵¹ anni. RFC 8439 descrive l'algoritmo e la sua analisi.
Un record
Siamo contenti?
Pulp Fiction
Abbiamo salvato la password, ma per accedere a un sito web di solito sono necessari anche un nome utente e un indirizzo. A volte abbiamo bisogno di una nota accanto a loro. Se memorizziamo questi valori separatamente, l'applicazione deve in qualche modo ricordare che appartengono insieme. Mettiamo questi “documenti” in una busta. Combiniamo i campi in un'unica raccolta e la chiamiamo record.
Un record può contenere qualsiasi combinazione di campi. L'applicazione li assembla in un unico pacchetto, lo comprime e crittografa il tutto utilizzando la costruzione che abbiamo appena descritto. I valori dei campi, i nomi, i tipi e i tag sono tutti protetti. La modifica dell'indirizzo del sito web, anche mantenendo la stessa password, crea una nuova versione dell'intero record, crittografato con un nuovo record_key.
Catena di hash
IL TUO PROGETTO DOVREBBE UTILIZZARE UNA BLOCKCHAIN? → NO
XKCD2267
Qualcuno che conosce secret_key può preparare un record crittografato per il nostro caveau. Per evitare di accettarlo come nostro, l'applicazione firma ogni versione del record con signing_key. Senza questa chiave, un estraneo non può creare una versione con una firma che il server e altri dispositivi accetteranno.
La versione crittografata necessita di alcune informazioni di accompagnamento: a quale archivio e record appartiene, quando è cambiata e quali versioni la precedono. Questo costituisce l'intestazione pubblica. Non contiene la password, ma necessita comunque di protezione: altrimenti qualcuno potrebbe modificare la proprietà e le relazioni degli oggetti senza toccare il testo cifrato. L'applicazione firma i byte esatti dell'intestazione insieme al testo cifrato. La modifica dei dati firmati invalida la firma.
Il server ricorda un magazzino di pacchi sigillati. Vede l'intestazione, il contenuto crittografato e la firma, ma non la password all'interno, e sa che il destinatario esiste in città. Controlla la firma rispetto alla parte pubblica bloccata di signing_key e decide se accettare la nuova versione. Non ha bisogno né della password principale né di secret_key né di alcuna chiave privata. Durante la lettura, il client controlla la stessa firma, quindi controlla nuovamente il collegamento tra il contenuto e l'intestazione durante la decrittografia, utilizzando il tag di autenticazione HPKE.
La firma protegge anche i collegamenti tra le versioni. Un hash SHA-256 dell'intestazione esatta e dei byte del testo cifrato diventa l'identificatore della versione. L'intestazione della versione successiva contiene l'identificatore della versione precedente. Quando modifichiamo la password di un sito web, l'applicazione crea e firma un nuovo oggetto con questo collegamento. Il vecchio oggetto e la sua firma rimangono invariati. Pertanto, sostituire il predecessore preservando la firma della nuova versione fallirà. Se due dispositivi apportano modifiche indipendenti, entrambi i rami sopravvivono; una versione unita può fare riferimento a entrambi.
Ricordi la blockchain? Eccolo :-).
L'intestazione pubblica ci consente di controllare i collegamenti senza decrittografarli, ma alcune informazioni rimangono visibili. Il server conosce gli identificatori, i collegamenti alla versione, l'ora di modifica fornita dal client, il numero di campi e la dimensione codificata di ciascun valore di campo. La crittografia nasconde il contenuto del record, non queste informazioni strutturali.
Sincronizzazione
"Non una fuga di dati, un backup distribuito gratuitamente."
Meme di Internet.
Per funzionare su più dispositivi, dobbiamo trasferire la serratura e registrare la cronologia tra di loro. Il formato dell'oggetto non cambia: trasferiamo gli stessi byte crittografati e le stesse firme archiviate localmente.
Per il trasferimento manuale, l'applicazione crea un archivio ZIP contenente il blocco, tutte le versioni dei record crittografati archiviate localmente e un elenco di file firmati. Non sono inclusi né la password né secret_key. Su un altro dispositivo, l'applicazione prima apre la serratura, quindi controlla le firme, decrittografa i record e ripristina una copia del vault locale. L'importazione è destinata a un dispositivo in cui questo deposito non esiste ancora; non unisce la cronologia con una copia esistente.
Un server di sincronizzazione scambia le modifiche tra copie di lavoro. Al primo caricamento, riceve il lucchetto firmato, controlla la firma e calcola l'indirizzo del caveau pubblico dalla parte pubblica di signing_key. Quindi ricorda questa chiave e l'identificatore del vault dall'intestazione: questo registra il vault sul server. Le modifiche successive devono corrispondere a questo identificatore e portare la firma della stessa chiave.
Un nuovo dispositivo necessita dell'indirizzo del vault e di secret_key, quindi l'applicazione li combina in un codice di trasferimento con un checksum. L'utente trasferisce questo codice al dispositivo e inserisce separatamente la password principale; solo l'indirizzo pubblico viene inviato al server.
Dopo aver aperto il lucchetto scaricato, l'applicazione confronta le parti pubbliche delle chiavi recuperate con secret_key e l'intestazione e confronta l'indirizzo calcolato con il codice di trasferimento. Quindi salva la parte pubblica verificata di signing_key per controllare i record.
Durante la sincronizzazione, l'applicazione recupera le versioni mancanti e carica le modifiche. Il server controlla le firme e i collegamenti alle versioni precedenti, memorizza gli oggetti accettati e li trasmette ad altri dispositivi. Il controllo dei dati ricevuti, la loro decrittografia e la risoluzione dei conflitti avvengono tutti sui dispositivi dell'utente.
La registrazione non richiede nome, indirizzo e-mail o password separata. Il server vede l'indirizzo pubblico, la chiave di verifica della firma, le intestazioni pubbliche, le dimensioni degli oggetti e i collegamenti alla versione. Vede anche gli indirizzi IP e gli orari delle richieste. La password principale, secret_key, le chiavi private e il contenuto dei record di testo normale rimangono sui dispositivi dell'utente. Chiunque conosca l’indirizzo pubblico può scaricare gli oggetti crittografati, ma per leggerli sono necessari i segreti dell’utente.
Questi controlli rilevano oggetti contraffatti, ma non dimostrano che il server abbia mostrato la cronologia completa e il suo stato più recente. Può nascondere un ramo che il client non ha mai visto o restituire un set di dati precedente coerente. Una firma conferma l’autenticità di un singolo oggetto, non la completezza della risposta del server. L'utente potrebbe notare modifiche mancanti su un altro dispositivo, ma una cronologia precedente coerente supererà la verifica hash-chain.
Modifica della password principale
Quindi, Vostra Santità vorrebbe cambiare la sua password?
Hacker (1995)
La modifica della password principale crittografa nuovamente solo il lucchetto; master_key all'interno rimane lo stesso. I record, le loro firme, secret_key e l'indirizzo del vault rimangono quindi invariati. Il nuovo lucchetto è firmato con lo stesso signing_key.
Una vecchia copia del lucchetto si apre ancora con la vecchia password e lo stesso secret_key; la modifica della password non revoca le chiavi già esposte.
Il quadro completo
Il blocco protegge master_key, message_key crittografa i record e signing_key crea firme. Gli archivi ZIP e il server ricevono oggetti crittografati con firme.