Hur kan en server lagra och synkronisera våra hemligheter utan att hålla i nycklarna för att läsa dem? Vi kommer att följa hur Confidex skapar sina nycklar, skyddar dem i ett lås och använder dem för att kryptera data och verifiera ändringar.
Lås
Bara den Utvalde kan öppna dörren
Matrisen laddas om
Föreställ dig att vi bygger en virtuell vault för att lagra hemlig data. Den måste behålla dessa data på en bärbar dator efter att applikationen stängs och låta oss öppna den på en annan enhet. Valvet kommer att behöva sina egna nycklar. Vi måste lagra dem så att kopiering av filerna ger en utomstående ingen möjlighet att läsa eller ändra innehållet.
En lock skyddar nycklarna: en liten krypterad fil. För att öppna det krävs ett huvudlösenord och en hemlig kod, secret_key; inte heller finns i själva filen. Vi skulle kunna skriva ut hela låset i morgontidningen. Läsare skulle få en kopia, men publicering av den skulle inte avslöja de privata nycklarna.
Anta att en utomstående får den här filen och till och med lär sig huvudlösenordet. De kan nu upprepa varje beräkning som bara behöver lösenordet, men dessa beräkningar ger inte nyckeln som öppnar låset: secret_key är också en ingång. Filen innehåller ingen separat kontroll av enbart lösenordet. Att öppna den kräver fortfarande den okända hemliga koden.
Den koden kommer från kryptografiskt slumpmässiga data; en person uppfinner det inte. Så länge som secret_key är okänd, öppnar inte ens ett korrekt gissat huvudlösenord låset: angriparen saknar fortfarande en slumpmässig hemlighet. Enbart ett svagt lösenord gör därför inte serverkopian till en praktisk ordboksattack.
Om angriparen också får secret_key kan de testa gissningar av huvudlösenord offline. Detta är kostsamt, men den eventuella svårigheten beror på lösenordets styrka.
Nycklar
Mine herrar, vad gör nycklar? Nycklar... låsa upp saker?
Pirates of the Caribbean – Dead Man's Chest
Applikationen hämtar 32 slumpmässiga bytes från operativsystemets kryptografiska generator och förbereder dem för användning som nyckel. Dessa förberedda bytes är den privata huvudnyckeln, master_key.
Applikationen får alltid samma nycklar från master_key: att upprepa samma beräkningar på samma byte ger samma resultat. En bärbar dator och en telefon kan därför återställa identiska nycklar. För att få master_key på en ny enhet måste appen först öppna låset med huvudlösenordet och secret_key.
Det första paret: kryptering
Jag är nyckelmästaren.
Ghostbusters
För att lagra krypterad data beräknar applikationen en offentlig nyckel från master_key: secret_key. Den kan kryptera data för valvet, medan dekryptering kräver master_key.
"Public" beskriver denna nyckels roll i algoritmen, inte en skyldighet att ge den till alla. I Confidex stannar den hos användaren. Det är också den andra hemligheten som behövs för att öppna låset, så det förblir lokalt och skickas inte över nätverket. På egen hand låter secret_key någon kryptera data för valvet, men inte dekryptera det eller signera ändringar för dess räkning.
X25519 är algoritmen som tar master_key och beräknar secret_key. Det är praktiskt taget omöjligt att återställa master_key från secret_key. En klassisk attack på en imaginär superdator som utför en miljard miljarder kurvoperationer per sekund skulle ta cirka 2,3 biljoner år i genomsnitt (SafeCurves-uppskattning).
Det andra paret: signaturer
Det enda som är original är signaturen
Fånga mig om du kan
Det första paret hjälper till att dölja innehållet. Vi behöver nu ett sätt att bekräfta att valvets ägare godkände en ändring. Servern måste också kunna kontrollera signaturer, så vi måste skicka en offentlig verifieringsnyckel till den. Det första parets offentliga del är secret_key, vilket servern inte får ta emot. Vi skapar därför ett andra, permanent nyckelpar för signaturer.
Istället för att lagra en annan oberoende hemlighet hämtar vi en privat signeringsnyckel från master_key och kallar den signing_key. Applikationen använder den för att signera data och låset. Andra enheter och servern använder sin publika del för att kontrollera signaturer utan att läsa innehållet.
Vi kan återställa detta par från samma master_key. Applikationen härleder ett startvärde från det: 32 byte som unikt bestämmer signeringsnyckeln.
För att härleda olika nycklar för olika jobb inkluderar vi en fast etikett i beräkningen: här, confidex/vault/ed25519/v1. Samma master_key och samma etikett producerar alltid samma nyckel.
Huvudlösenordet har inte angetts i dessa beräkningar. Båda paren beror på de slumpmässiga byten av master_key, inte på en sträng som uppfunnits av en person. Denna distinktion kommer att ha betydelse senare: lösenordet skyddar åtkomst till nycklar som redan finns, så vi kan ändra det utan att ändra själva nycklarna.
Härledningen använder en hash-baserad nyckelhärledningsfunktion: HKDF, HMAC-based Extract-and-Expand Key Derivation Function. Confidex använder det med SHA-256, därav namnet HKDF-SHA256. De resulterande 32 byten är seed för Ed25519-paret. Den offentliga delen av signing_key avslöjar inte secret_key, så vi kan skicka den till servern för signaturverifiering.
Vad är en låsfil och vad gör den?
Öppna pod Bay-dörrarna, HAL.
En rymdodyssé
Jag är ledsen, Dave. Jag är rädd att jag inte kan göra det.
Låset lagrar en krypterad master_key. För att öppna det krävs huvudlösenordet och secret_key. Den offentliga rubriken innehåller formatversionen, valvidentifieraren, den offentliga delen av signing_key, ett salt och parametrarna för lösenordsbearbetning. Den interna formatversionen är krypterad bredvid master_key.
När låset skapas har applikationen redan master_key, så den beräknar secret_key före kryptering. När användaren öppnar den anger secret_key separat: den saknas i den offentliga kopian av låset.
Efter dekryptering beräknar applikationen den offentliga delen av master_key med X25519 och kräver att den matchar secret_key. Den återställer sedan signing_key och jämför dess offentliga del med nyckeln i rubriken. På så sätt kontrollerar låset sig självt.
För att sakta ner gissning av huvudlösenord måste varje försök kosta tid och minne. Confidex använder Argon2id med 64 MiB minne och tre pass. Lösenordsbearbetning använder också ett slumpmässigt salt. Saltet är inte hemligt: det gör att samma lösenord ger olika resultat i olika lås.
Om secret_key redan är känd ändrar lösenordslängden dramatiskt omfattningen av en attack. Det finns cirka 12 miljoner kombinationer av 5 slumpmässiga små latinska bokstäver, cirka 8 miljarder av 7 och cirka 9,5 × 10¹⁶ av 12. Att gå från 7 till 12 bokstäver ökar sökutrymmet nästan 12 miljoner gånger. Om sökningen på 7 bokstäver tog en dag i genomsnitt på en given maskin, skulle 5 ta cirka två minuter och 12 cirka 33 000 år.
För att få en nyckel från Argon2id-resultatet och secret_key använder vi funktionen HKDF-SHA256 som introducerades tidigare, med etiketten confidex/lock/wrap/v1. Resultatet är wrapping_key, nyckeln som krypterar låset.
Vi måste gömma master_key och skydda den från manipulering. XChaCha20-Poly1305 krypterar den med wrapping_key och lägger till en autentiseringstagg. Taggen binder chiffertexten till den offentliga rubriken: ett fel lösenord eller en ändring av de skyddade byten gör att öppningen misslyckas. Taggen och ett slumpmässigt engångsvärde, nonce, lagras med chiffertexten.
Varje gång låset skapas eller krypteras igen, får applikationen ett nytt salt och nonce. Det krypterade innehållet inkluderar en versionsbyte och de 32 byten av master_key; en 24-byte nonce och en 16-byte-tagg lagras bredvid dem. algoritmens dokumentation beskriver konstruktionen.
En oöverensstämmelse mellan de återställda nycklarna kan bero på ett avsiktligt felaktigt lås eller en bugg i programmet som skapade det. Slumpmässig bytekorruption upptäcks vanligtvis tidigare när taggen kontrolleras.
Servern har inte de hemligheter som behövs för att kontrollera denna tagg. För att överföra låset lägger applikationen därför till en signatur gjord med signing_key. Den täcker hela den färdiga filen, inklusive den offentliga rubriken och krypterat innehåll. Servern kontrollerar signaturen och, vid första uppladdningen, fäster den hela offentliga delen av signing_key och den interna valvets identifierare. Den kontrollerar efterföljande ändringar mot denna nyckel. Låset och krypterad data signeras med samma signing_key, återställd från master_key.
Valvet
Vi kan nu publicera låset. Endast någon som känner till secret_key och master password kan läsa master_key, medan vem som helst kan ta den offentliga signeringsnyckeln från låsfilen och kontrollera signerad data utan att se innehållet i vare sig låset eller data.
Överväg ett lösenord, SuperPa55worD. För närvarande vill vi bara spara det: efter att ha stängt applikationen bör vi kunna skicka låsfilen och det krypterade lösenordet var som helst utan att någon ser SuperPa55worD, samtidigt som vi låter dem verifiera vårt författarskap. Kryptering måste vara reversibel när vi har secret_key och master password. Ett symmetriskt chiffer kan kryptera och dekryptera med samma nyckel. Eftersom vi inte lagrar den nyckeln måste vi kunna återställa den med låset. Vi kommer att kalla det message_key.
Vi kommer att få en separat message_key för varje ny kryptering. Vi måste fortfarande ordna återställningen: om du lämnar nyckeln öppen bredvid chiffertexten skulle alla med en kopia kunna läsa allt.
Från nya slumpmässiga bytes skapar applikationen ett tillfälligt X25519-par bara för denna kryptering: record_key. Den tar sedan den privata delen av record_key och valvets permanenta publika nyckel, secret_key, och beräknar en delad hemlighet. En separat funktion härleder message_key från den hemligheten; denna nyckel krypterar vårt lösenord.
Vid läsning tar applikationen en annan väg. Den använder master_key och den offentliga delen av record_key. Dessa två nycklar producerar samma delade hemlighet. Matematiskt multiplicerar båda rutterna samma initiala punkt på kurvan med två privata tal, i olika ordning. Beställningen ändrar inte resultatet. Vi kan därför återställa dekrypteringsnyckeln utan att behålla den privata delen av record_key.
Att erhålla denna hemlighet från de två publika nycklarna är praktiskt taget omöjligt: båda vägarna behöver motsvarande privata nyckel. De offentliga punkterna låter oss inte effektivt återställa de privata nummer som producerade dem. Detta matematiska trick underbygger valvets säkerhet. Men det hindrar inte någon från att ge oss ett annat valv med vårt lås fäst vid det.
Vi kan nu lagra chiffertexten tillsammans med den offentliga delen av record_key. Den privata delen av record_key och den använda message_key behövs inte längre för framtida läsning. Senare återställer applikationen dekrypteringsnyckeln från master_key och den lagrade offentliga delen av record_key. En permanent valvhemlighet kan alltså dekryptera många separat krypterade värden, var och en med sina egna message_key återställda vid behov.
Det räcker inte att dölja texten: vi måste också upptäcka manipulering när vi läser den. Kryptering lägger därför till en autentiseringstagg, ett kort värde som kontrolleras tillsammans med den krypterade datan. Om kontrollen misslyckas avvisar programmet resultatet. Den får inte visa slumpmässig eller ändrad text som ett framgångsrikt dekrypterat lösenord.
Vi har beskrivit en konstruktion där ett asymmetriskt par hjälper till att etablera en delad hemlighet, medan message_key krypterar innehållet. Detta kallas Hybrid Public Key Encryption eller HPKE och anges i RFC 9180. Confidex använder X25519 för att komma överens om hemligheten, HKDF-SHA256 för att härleda nycklar och ChaCha20-Poly1305 för kryptering med integritetskontroll. Här tar HKDF den delade hemligheten som överenskommits med record_key och master_key-paret; detta är ett separat jobb från att härleda Ed25519 seed. RFC 5869 beskriver HKDF.
För nästa kryptering skapar programmet ett nytt record_key och ett nytt HPKE sammanhang. Återanvändning av en kombination av message_key och nonce bryter chifferns skydd. I Confidex härleds nyckeln och nonce inuti HPKE, används för ett meddelande, och sedan förkastas sammanhanget.
ChaCha20 själv använder en 256-bitars message_key. För att få en känsla av en direkt attack, föreställ dig en fiktiv superdator som testar en miljard miljarder kandidater per sekund. Att söka efter de 2²⁵⁶ möjliga nycklarna skulle ta cirka 1,8 × 10⁵¹ år. RFC 8439 beskriver algoritmen och dess analys.
Ett rekord
Nöjda?
Pulp Fiction
Vi har sparat lösenordet, men för att logga in på en webbplats krävs oftast också ett användarnamn och en adress. Ibland behöver vi en lapp bredvid dem. Om vi lagrar dessa värden separat måste applikationen på något sätt komma ihåg att de hör ihop. Låt oss lägga dessa "papper" i ett kuvert. Vi kombinerar fälten till en samling och kallar det en record.
En post kan innehålla vilken kombination av fält som helst. Applikationen sätter ihop dem till ett paket, komprimerar det och krypterar helheten med den konstruktion vi just har beskrivit. Fältvärden, namn, typer och taggar är alla skyddade. Om du ändrar webbadressen, även om du behåller samma lösenord, skapas en ny version av hela posten, krypterad med en ny record_key.
Hash-kedja
SKA DITT PROJEKT ANVÄNDA EN BLOCKCHAIN? → NEJ
XKCD 2267
Någon som känner till secret_key kan förbereda en krypterad post för vårt valv. För att undvika att acceptera den som vår signerar applikationen varje postversion med signing_key. Utan denna nyckel kan en utomstående inte skapa en version med en signatur som servern och andra enheter accepterar.
Den krypterade versionen behöver lite åtföljande information: vilket valv och post den tillhör, när den ändrades och vilka versioner som föregår den. Detta bildar den offentliga rubriken. Det innehåller inte lösenordet, men det behöver fortfarande skydd: annars kan någon ändra objektens äganderätt och relationer utan att röra chiffertexten. Applikationen signerar de exakta rubrikbytena tillsammans med chiffertexten. Om du ändrar den signerade informationen blir signaturen ogiltig.
Servern liknar ett lager av förseglade paket. Den ser rubriken, krypterat innehåll och signatur, men inte lösenordet inuti, och den vet att mottagaren finns i staden. Den kontrollerar signaturen mot den fästa offentliga delen av signing_key och bestämmer om den ska acceptera den nya versionen. Den behöver varken huvudlösenordet eller secret_key eller någon privat nyckel. Vid läsning kontrollerar klienten samma signatur och kontrollerar sedan bindningen mellan innehållet och rubriken igen under dekryptering, med hjälp av autentiseringstaggen HPKE.
Signaturen skyddar även länkarna mellan versioner. En SHA-256 hash av den exakta rubriken och chiffertextbyten blir versionens identifierare. Nästa versions rubrik innehåller den tidigare versionens identifierare. När vi ändrar ett webbadresslösenord skapar applikationen och signerar ett nytt objekt med denna länk. Det gamla föremålet och dess signatur förblir oförändrade. Att ersätta föregångaren samtidigt som den nya versionens signatur bevaras kommer därför att misslyckas. Om två enheter gör oberoende förändringar överlever båda grenarna; en sammanslagen version kan hänvisa till båda.
Kommer du ihåg blockchain? Där är den :-).
Den offentliga rubriken låter oss kontrollera länkar utan att dekryptera, men viss information förblir synlig. Servern känner till identifierarna, versionslänkarna, ändringstiden som tillhandahålls av klienten, antalet fält och varje fältvärdes kodade storlek. Kryptering döljer postens innehåll, inte denna strukturella information.
Synkronisera
"Inte en dataläcka, en gratis distribuerad säkerhetskopia."
Internet meme.
För att arbeta på flera enheter måste vi överföra låset och spela in historik mellan dem. Objektformatet ändras inte: vi överför samma krypterade bytes och signaturer som lagras lokalt.
För manuell överföring skapar applikationen ett ZIP-arkiv som innehåller låset, alla lokalt lagrade krypterade postversioner och en signerad fillista. Varken lösenordet eller secret_key ingår. På en annan enhet öppnar applikationen först låset, kontrollerar sedan signaturer, dekrypterar posterna och återställer en lokal valvkopia. Importen är avsedd för en enhet där detta valv ännu inte finns; den slår inte ihop historiken med en befintlig kopia.
En synkserver utbyter ändringar mellan arbetskopior. Vid första uppladdningen tar den emot det signerade låset, kontrollerar signaturen och beräknar adressen till det offentliga valvet från den offentliga delen av signing_key. Den kommer sedan ihåg denna nyckel och valvets identifierare från huvudet: detta registrerar valvet på servern. Efterföljande ändringar måste matcha denna identifierare och ha en signatur från samma nyckel.
En ny enhet behöver valvets adress och secret_key, så applikationen kombinerar dem till en överföringskod med en kontrollsumma. Användaren överför denna kod till enheten och anger huvudlösenordet separat; endast den offentliga adressen skickas till servern.
Efter att ha öppnat det nedladdade låset jämför applikationen de offentliga delarna av de återställda nycklarna med secret_key och rubriken och kontrollerar den beräknade adressen mot överföringskoden. Den sparar sedan den verifierade offentliga delen av signing_key för att kontrollera poster.
Under synkroniseringen hämtar applikationen saknade versioner och laddar upp sina ändringar. Servern kontrollerar signaturer och länkar till tidigare versioner, lagrar accepterade objekt och skickar dem till andra enheter. Att kontrollera mottagen data, dekryptera den och lösa konflikter sker allt på användarens enheter.
Registreringen kräver inget namn, e-postadress eller separat lösenord. Servern ser den offentliga adressen, signaturverifieringsnyckeln, publika rubriker, objektstorlekar och versionslänkar. Den ser också IP-adresser och förfrågningstider. Huvudlösenordet, secret_key, privata nycklar och innehållet i klartextposter stannar på användarens enheter. Alla som känner till den allmänna adressen kan ladda ner de krypterade objekten, men att läsa dem kräver användarens hemligheter.
Dessa kontroller upptäcker förfalskade objekt, men bevisar inte att servern har visat hela historiken och dess senaste tillstånd. Det kan dölja en gren som klienten aldrig har sett eller returnera en konsekvent äldre datauppsättning. En signatur bekräftar ett enskilt objekts äkthet, inte fullständigheten i serverns svar. Användaren kan märka att ändringar saknas på en annan enhet, men en konsekvent gammal historik kommer att klara hash-chain-verifieringen.
Ändra huvudlösenordet
Så, skulle din helighet bry sig om att ändra hennes lösenord?
Hackare (1995)
Att ändra huvudlösenordet omkrypterar endast låset; master_key inuti förblir densamma. Poster, deras signaturer, secret_key och valvadressen förblir därför oförändrade. Det nya låset signeras med samma signing_key.
En gammal låskopia öppnas fortfarande med det gamla lösenordet och samma secret_key; att ändra lösenordet återkallar inte nycklar som redan har exponerats.
Hela bilden
Låset skyddar master_key, message_key krypterar poster och signing_key skapar signaturer. ZIP-arkiv och servern tar emot krypterade objekt med signaturer.