Cum poate un server să stocheze și să ne sincronizeze secretele fără a ține cheile pentru a le citi? Vom urmări cum Confidex își creează cheile, le protejează într-un lacăt și le folosește pentru a cripta datele și a verifica modificările.
Lacăt
Numai Alesul poate deschide ușa
The Matrix Reloaded
Imaginați-vă că construim un vault virtual pentru a stoca date secrete. Trebuie să păstreze acele date pe un laptop după ce aplicația se închide și să ne lase să o deschidem pe alt dispozitiv. Seiful va avea nevoie de propriile chei. Trebuie să le stocăm astfel încât copierea fișierelor să nu ofere unui străin să nu le citească sau să le modifice conținutul.
Un lock protejează cheile: un fișier mic criptat. Deschiderea ei necesită o parolă principală și un cod secret, secret_key; nici nu se află în fișierul în sine. Am putea tipări întregul lacăt în ziarul de dimineață. Cititorii ar primi o copie, dar publicarea acesteia nu ar dezvălui cheile private.
Să presupunem că un străin primește acest fișier și chiar învață parola principală. Acum pot repeta fiecare calcul care are nevoie doar de parolă, dar acele calcule nu vor produce cheia care deschide încuietoarea: secret_key este, de asemenea, o intrare. Fișierul nu conține o verificare separată a parolei. Deschiderea acestuia necesită încă codul secret necunoscut.
Acest cod provine din date aleatorii criptografic; o persoană nu o inventează. Atâta timp cât secret_key este necunoscut, chiar și o parolă principală ghicită corect nu deschide lacătul: atacatorului îi lipsește încă un secret aleatoriu. Prin urmare, o singură parolă slabă nu transformă copia serverului într-un atac practic de dicționar.
Dacă atacatorul obține, de asemenea, secret_key, poate testa offline ipotezele privind parola principală. Acest lucru este costisitor, dar eventuala dificultate depinde de puterea parolei.
Chei
Domnilor, ce fac cheile? Cheile... deblochează lucruri?
Pirații din Caraibe – Cufărul omului mort
Aplicația obține 32 de octeți aleatori de la generatorul criptografic al sistemului de operare și îi pregătește pentru utilizare ca cheie. Acești octeți pregătiți sunt cheia principală privată, master_key.
Aplicația derivă întotdeauna aceleași chei de la master_key: repetarea acelorași calcule pe aceiași octeți dă același rezultat. Prin urmare, un laptop și un telefon pot recupera chei identice. Pentru a obține master_key pe un dispozitiv nou, aplicația trebuie să deschidă mai întâi lacătul cu parola principală și secret_key.
Prima pereche: criptare
Eu sunt directorul cheie.
Ghostbusters
Pentru a stoca date criptate, aplicația calculează o cheie publică din master_key: secret_key. Poate cripta datele pentru seif, în timp ce decriptarea necesită master_key.
„Public” descrie rolul acestei chei în algoritm, nu o obligație de a o acorda tuturor. În Confidex rămâne cu utilizatorul. Este, de asemenea, al doilea secret necesar pentru a deschide lacătul, deci rămâne local și nu este trimis prin rețea. Pe cont propriu, secret_key permite cuiva să cripteze datele pentru seif, dar nu să le decripteze sau să semneze modificări în numele său.
X25519 este algoritmul care ia master_key și calculează secret_key. Recuperarea master_key de la secret_key este practic imposibilă. Un atac clasic asupra unui supercomputer imaginar care efectuează un miliard de miliarde de operații pe secundă ar dura aproximativ 2,3 × 10¹² ani în medie (estimarea SafeCurves).
A doua pereche: semnături
Singurul lucru original este semnătura
Prinde-mă dacă poți
Prima pereche ajută la ascunderea conținutului. Acum avem nevoie de o modalitate de a confirma că proprietarul seifului a autorizat o modificare. Serverul trebuie să poată verifica și semnăturile, așa că trebuie să îi trimitem o cheie publică de verificare. Partea publică a primei perechi este secret_key, pe care serverul nu trebuie să o primească. Prin urmare, creăm o a doua pereche de chei permanentă pentru semnături.
În loc să stocăm un alt secret independent, obținem o cheie privată de semnare de la master_key și o numim signing_key. Aplicația îl folosește pentru a semna date și lacăt. Alte dispozitive și serverul își folosesc partea publică pentru a verifica semnăturile fără a citi conținutul.
Putem recupera această pereche de la același master_key. Aplicația derivă din aceasta o valoare de pornire: 32 de octeți care determină în mod unic cheia de semnare.
Pentru a obține chei diferite pentru diferite lucrări, includem o etichetă fixă în calcul: aici, confidex/vault/ed25519/v1. Aceeași master_key și aceeași etichetă produc întotdeauna aceeași cheie.
Parola principală nu a introdus aceste calcule. Ambele perechi depind de octeții aleatori ai lui master_key, nu de un șir inventat de o persoană. Această distincție va conta mai târziu: parola va proteja accesul la cheile care există deja, astfel încât să o putem schimba fără a schimba cheile în sine.
Derivarea folosește o funcție de derivare a cheilor bazată pe hash: HKDF, HMAC-based Extract-and-Expand Key Derivation Function. Confidex îl folosește cu SHA-256, de unde și numele HKDF-SHA256. Cei 32 de octeți rezultați sunt seed pentru perechea Ed25519. Partea publică a signing_key nu dezvăluie secret_key, așa că îl putem trimite la server pentru verificarea semnăturii.
Ce este un fișier de blocare și ce face el?
Deschide ușile podului, HAL.
O odisee a spațiului
Îmi pare rău, Dave. Mi-e teamă că nu pot face asta.
Lacătul stochează un master_key criptat. Deschiderea acestuia necesită parola principală și secret_key. Antetul public conține versiunea formatului, identificatorul seifului, partea publică a signing_key, un salt și parametrii de procesare a parolei. Versiunea formatului intern este criptată alături de master_key.
La crearea blocării, aplicația are deja master_key, așa că calculează secret_key înainte de criptare. La deschiderea acestuia, utilizatorul furnizează separat secret_key: este absent din copia publică a lacătului.
După decriptare, aplicația calculează partea publică a master_key folosind X25519 și solicită ca aceasta să se potrivească cu secret_key. Apoi recuperează signing_key și compară partea sa publică cu cheia din antet. În acest fel, încuietoarea se verifică de la sine.
Pentru a încetini ghicirea parolei principale, fiecare încercare trebuie să coste timp și memorie. Confidex folosește Argon2id cu 64 MiB de memorie și trei treceri. Procesarea parolei folosește și o sare aleatorie. Sarea nu este secretă: face ca aceeași parolă să producă rezultate diferite în încuietori diferite.
Dacă secret_key este deja cunoscut, lungimea parolei schimbă dramatic amploarea unui atac. Există aproximativ 12 milioane de combinații de 5 litere latine mici aleatorii, aproximativ 8 miliarde din 7 și aproximativ 9,5 × 10¹⁶ din 12. Trecerea de la 7 la 12 litere mărește spațiul de căutare de aproape 12 milioane de ori. Dacă căutarea a 7 litere ar dura în medie o zi pe o anumită mașină, 5 ar dura aproximativ două minute, iar 12 aproximativ 33.000 de ani.
Pentru a obține o cheie din rezultatul Argon2id și secret_key, folosim funcția HKDF-SHA256 introdusă mai devreme, cu eticheta confidex/lock/wrap/v1. Rezultatul este wrapping_key, cheia care criptează lacătul.
Trebuie să ascundem master_key și să-l protejăm de manipulare. XChaCha20-Poly1305 îl criptează cu wrapping_key și adaugă o etichetă de autentificare. Eticheta leagă textul cifrat la antetul public: o parolă greșită sau o modificare a octeților protejați face ca deschiderea să eșueze. Eticheta și o valoare aleatorie de unică folosință, nonce, sunt stocate cu textul cifrat.
De fiecare dată când lacătul este creat sau criptat din nou, aplicația obține o sare proaspătă și nonce. Conținutul criptat include un octet de versiune și cei 32 de octeți ai master_key; o etichetă de 24 de octeți nonce și o etichetă de 16 octeți sunt stocate alături de ele. Documentația algoritmului descrie construcția.
O nepotrivire între cheile recuperate poate rezulta dintr-o încuietoare deliberată defectuoasă sau dintr-o eroare în programul care a creat-o. Coruperea aleatoare de octeți este de obicei detectată mai devreme, când eticheta este verificată.
Serverul nu are secretele necesare pentru a verifica această etichetă. Pentru a transfera lacătul, aplicația adaugă așadar o semnătură făcută cu signing_key. Acesta acoperă întregul fișier finalizat, inclusiv antetul public și conținutul criptat. Serverul verifică semnătura și, la prima încărcare, fixează întreaga parte publică a signing_key și identificatorul intern al seifului. Verifică modificările ulterioare cu această cheie. Blocarea și datele criptate sunt semnate cu același signing_key, recuperat de la master_key.
Bolta
Acum putem publica blocarea. Doar cineva care cunoaște secret_key și master password poate citi master_key, în timp ce oricine poate lua cheia publică de semnare din fișierul de lacăt și poate verifica datele semnate fără a vedea conținutul fie al lacătului, fie al datelor.
Luați în considerare o parolă, SuperPa55worD. Deocamdată, vrem doar să-l salvăm: după închiderea aplicației, ar trebui să putem trimite fișierul de blocare și parola criptată oriunde fără ca nimeni să vadă SuperPa55worD, permițându-i totuși să ne verifice autoritatea. Criptarea trebuie să fie reversibilă când avem secret_key și master password. Un cifru simetric poate cripta și decripta cu aceeași cheie. Deoarece nu stocăm cheia respectivă, trebuie să o putem recupera folosind încuietoarea. Îl vom numi message_key.
Vom obține un message_key separat pentru fiecare nouă criptare. Încă trebuie să organizăm recuperarea acestuia: lăsând cheia deschisă lângă textul cifrat ar permite oricui are o copie să citească totul.
Din octeți aleatori noi, aplicația creează o pereche temporară X25519 doar pentru această criptare: record_key. Apoi ia partea privată a record_key și cheia publică permanentă a seifului, secret_key, și calculează un secret partajat. O funcție separată derivă message_key din acel secret; această cheie criptează parola noastră.
La citire, aplicația ia o altă rută. Folosește master_key și partea publică a record_key. Aceste două chei produc același secret comun. Matematic, ambele rute înmulțesc același punct inițial al curbei cu două numere private, într-o ordine diferită. Ordinea nu schimbă rezultatul. Prin urmare, putem recupera cheia de decriptare fără a păstra partea privată a record_key.
Obținerea acestui secret de la cele două chei publice este practic imposibilă: oricare rută are nevoie de cheia privată corespunzătoare. Punctele publice nu ne lasă să recuperăm eficient numerele private care le-au produs. Acest truc matematic susține securitatea seifului. Dar nu oprește pe cineva să ne dea o altă seif cu încuietoarea atașată de ea.
Acum putem stoca textul cifrat alături de partea publică a record_key. Partea privată a lui record_key și message_key folosit nu mai sunt necesare pentru citirea viitoare. Mai târziu, aplicația recuperează cheia de decriptare de la master_key și partea publică stocată a record_key. Un secret permanent de seif poate decripta astfel multe valori criptate separat, fiecare cu propriul message_key recuperat atunci când este necesar.
Ascunderea textului nu este suficientă: trebuie să detectăm și manipularea atunci când îl citim. Prin urmare, criptarea adaugă o etichetă de autentificare, o valoare scurtă verificată împreună cu datele criptate. Dacă verificarea eșuează, aplicația respinge rezultatul. Nu trebuie să afișeze text aleatoriu sau modificat ca parolă decriptată cu succes.
Am descris o construcție în care o pereche asimetrică ajută la stabilirea unui secret comun, în timp ce message_key criptează conținutul. Acesta se numește Hybrid Public Key Encryption sau HPKE și este specificat în RFC 9180. Confidex folosește X25519 pentru a conveni asupra secretului, HKDF-SHA256 pentru a obține cheile și ChaCha20-Poly1305 pentru criptare cu verificarea integrității. Aici, HKDF preia secretul partajat agreat folosind record_key și perechea master_key; aceasta este o sarcină separată de derivarea Ed25519 seed. RFC 5869 descrie HKDF.
Pentru următoarea criptare, aplicația creează un nou context record_key și un nou context HPKE. Reutilizarea unei combinații de message_key și nonce rupe protecția cifrului. În Confidex, cheia și nonce sunt derivate în HPKE, utilizate pentru un mesaj, iar apoi contextul este eliminat.
ChaCha20 în sine folosește un message_key pe 256 de biți. Pentru a înțelege un atac direct, imaginați-vă un supercomputer fictiv care testează un miliard de miliarde de candidați pe secundă. Căutarea celor 2²⁵⁶ chei posibile ar dura aproximativ 1,8 × 10⁵¹ ani. RFC 8439 descrie algoritmul și analiza acestuia.
Un record
Suntem fericiți?
Pulp Fiction
Am salvat parola, dar autentificarea pe un site web necesită de obicei și un nume de utilizator și o adresă. Uneori avem nevoie de o notă lângă ele. Dacă stocăm aceste valori separat, aplicația trebuie să-și amintească cumva că aparțin împreună. Să punem aceste „hârtii” într-un plic. Combinăm câmpurile într-o singură colecție și o numim record.
O înregistrare poate conține orice combinație de câmpuri. Aplicația le asamblează într-un singur pachet, îl comprimă și criptează întregul folosind construcția pe care tocmai am descris-o. Valorile câmpurilor, numele, tipurile și etichetele sunt toate protejate. Schimbarea adresei site-ului web, chiar dacă păstrați aceeași parolă, creează o nouă versiune a întregii înregistrări, criptată cu un nou record_key.
Hash-lanț
PROIECTUL TĂU TREBUIE UTILIZA UN BLOCKCHAIN? → NU
XKCD 2267
Cineva care cunoaște secret_key poate pregăti o înregistrare criptată pentru seiful nostru. Pentru a evita acceptarea acesteia ca a noastră, aplicația semnează fiecare versiune de înregistrare cu signing_key. Fără această cheie, un străin nu poate crea o versiune cu o semnătură pe care serverul și alte dispozitive o vor accepta.
Versiunea criptată are nevoie de câteva informații însoțitoare: cărei seif și înregistrare îi aparține, când s-a schimbat și ce versiuni o preced. Acesta formează antetul public. Nu conține parola, dar mai are nevoie de protecție: altfel cineva ar putea schimba proprietatea și relațiile obiectelor fără a atinge textul cifrat. Aplicația semnează octeții de antet exact împreună cu textul cifrat. Modificarea datelor semnate invalidează semnătura.
Serverul seamănă cu un depozit de colete sigilate. Vede antetul, conținutul criptat și semnătura, dar nu și parola din interior și știe că destinatarul există în oraș. Verifică semnătura față de partea publică fixată a signing_key și decide dacă acceptă noua versiune. Nu are nevoie nici de parola principală, nici de secret_key și nici de vreo cheie privată. La citire, clientul verifică aceeași semnătură, apoi verifică din nou legătura dintre conținut și antet în timpul decriptării, folosind eticheta de autentificare HPKE.
Semnătura protejează, de asemenea, legăturile dintre versiuni. Un hash SHA-256 cu antetul exact și octeții de text cifrat devine identificatorul versiunii. Antetul versiunii următoare conține identificatorul versiunii anterioare. Când schimbăm parola unui site web, aplicația creează și semnează un nou obiect cu acest link. Obiectul vechi și semnătura lui rămân neschimbate. Prin urmare, înlocuirea predecesorului păstrând semnătura noii versiuni va eșua. Dacă două dispozitive fac modificări independente, ambele ramuri supraviețuiesc; o versiune îmbinată se poate referi la ambele.
Îți amintești de blockchain? Acolo este :-).
Antetul public ne permite să verificăm linkurile fără a decripta, dar unele informații rămân vizibile. Serverul cunoaște identificatorii, legăturile de versiuni, timpul de schimbare furnizat de client, numărul de câmpuri și dimensiunea codificată a fiecărui câmp. Criptarea ascunde conținutul înregistrării, nu aceste informații structurale.
Sincronizare
„Nu o scurgere de date, un backup distribuit gratuit.”
meme de internet.
Pentru a lucra pe mai multe dispozitive, trebuie să transferăm istoricul de blocare și înregistrare între ele. Formatul obiectului nu se schimbă: transferăm aceiași octeți și semnături criptați care sunt stocați local.
Pentru transferul manual, aplicația creează o arhivă ZIP care conține blocarea, toate versiunile de înregistrări criptate stocate local și o listă de fișiere semnată. Nici parola, nici secret_key nu sunt incluse. Pe un alt dispozitiv, aplicația deschide mai întâi lacătul, apoi verifică semnăturile, decriptează înregistrările și restaurează o copie a seifului local. Importul este destinat unui dispozitiv în care acest seif nu există încă; nu îmbină istoricul cu o copie existentă.
Un server de sincronizare schimbă modificări între copiile de lucru. La prima încărcare, primește blocarea semnată, verifică semnătura și calculează adresa seifului public din partea publică a signing_key. Apoi își amintește această cheie și identificatorul seifului din antet: aceasta înregistrează seiful pe server. Modificările ulterioare trebuie să se potrivească cu acest identificator și să poarte o semnătură de la aceeași cheie.
Un dispozitiv nou are nevoie de adresa seifului și secret_key, așa că aplicația le combină într-un cod de transfer cu o sumă de control. Utilizatorul transferă acest cod pe dispozitiv și introduce separat parola principală; doar adresa publică este trimisă la server.
După deschiderea lacătului descărcat, aplicația compară părțile publice ale cheilor recuperate cu secret_key și antetul și verifică adresa calculată cu codul de transfer. Apoi salvează partea publică verificată din signing_key pentru a verifica înregistrările.
În timpul sincronizării, aplicația preia versiunile lipsă și își încarcă modificările. Serverul verifică semnăturile și legăturile către versiunile precedente, stochează obiectele acceptate și le transmite altor dispozitive. Verificarea datelor primite, decriptarea acestora și rezolvarea conflictelor se întâmplă toate pe dispozitivele utilizatorului.
Înregistrarea nu solicită niciun nume, adresă de e-mail sau parolă separată. Serverul vede adresa publică, cheia de verificare a semnăturii, anteturile publice, dimensiunile obiectelor și link-urile versiunii. De asemenea, vede adresele IP și orele de solicitare. Parola principală, secret_key, cheile private și conținutul înregistrărilor de text simplu rămân pe dispozitivele utilizatorului. Oricine cunoaște adresa publică poate descărca obiectele criptate, dar citirea lor necesită secretele utilizatorului.
Aceste verificări detectează obiectele falsificate, dar nu dovedesc că serverul a arătat istoricul complet și cea mai recentă stare. Poate ascunde o ramură pe care clientul nu a văzut-o niciodată sau poate returna un set de date mai vechi consistent. O semnătură confirmă autenticitatea unui obiect individual, nu caracterul complet al răspunsului serverului. Este posibil ca utilizatorul să observe modificări lipsă pe alt dispozitiv, dar un istoric vechi consistent va trece verificarea hash-chain.
Schimbarea parolei principale
Așadar, s-ar păsa sfinția ta să-i schimbe parola?
Hackeri (1995)
Schimbarea parolei principale re-criptează doar lacătul; master_key din interior rămâne același. Înregistrările, semnăturile lor, secret_key și adresa seifului rămân, prin urmare, neschimbate. Noua lacăt este semnată cu același signing_key.
O copie veche de blocare se deschide în continuare cu vechea parolă și aceeași secret_key; schimbarea parolei nu revocă cheile care au fost deja expuse.
Poza completă
Blocarea protejează master_key, message_key criptează înregistrările și signing_key creează semnături. ZIP arhivele și serverul primesc obiecte criptate cu semnături.