Nollställa lösenord på rätt sätt

HMAC, MJUKVARUARKITEKTUR

Stellan Söderström
27.10.2017

Det är otroligt vanligt med online-tjänster som erbjuder att nollställa ett glömt lösenord genom att skicka en engångs-länk till den e-postadress man registrerade i samband med att kontot skapades. Det är nästan lika vanligt med lösningar som är ganska komplicerade att implementera – helt i onödan. Först kommer ett exempel på hur man kanske intuitivt skulle bygga en sådan funktion, och därefter en mycket enklare och elegantare lösning.

Krav på lösningen

Några grundläggande säkerhetskrav behöver vi alltid ha med oss, oavsett hur vi väljer att implementera lösningen:

  • Lösenord får inte skickas i klartext
  • Samma länk får inte kunna användas till att byta lösenord mer än en gång
  • Länken får bara vara giltig i 24 timmar

Hur man inte bör göra

Med ovanstående säkerhetskrav på lösningen kommer en erfaren utvecklare ganska lätt komma fram till en fungerande lösning:

  1. Kunden matar in sin e-postadress
  2. Kolla i databasen att e-postadressen finns registrerad
  3. Skapa ett slumpmässigt, långt ID, en slags ”biljett” som är svår att lista ut om man inte är den som begärt den. En kryptografiskt säker slumptalsgenerator bör därför användas
  4. Spara ID, e-postadress, tidsstämpel och en flagga som anger att länken är ”oanvänd”
  5. Skicka e-postmeddelande med en länk på formen https://example.com/resetPassword?id=F39C0A16BC653B5DF39C0A16BC653B5D

När användaren fått e-postmeddelandet och klickar på länken kommer sedan följande process att köras:

  1. Slå upp e-postadress, tidsstämpel och flagga som anger att länken är oanvänd från databasen
  2. Kontrollera att tiden inte har passerat och att länken inte använts förut
  3. Erbjud användaren att mata in nytt lösenord
  4. Spara det nya lösenordet för användaren med lämplig kryptografiskt säker hashnings-algoritm
  5. Uppdatera databasen och sätt flaggan till använd.

För att inte databasen ska skräpas ned av gamla biljetter för att nollställa lösenord bör man också införa någon typ av batch-jobb som periodiskt rensar bort gamla biljetter

  1. En gång per dygn eller oftare, starta jobb
  2. Hämta alla biljetter som är använda och ta bort dem.
  3. Hämta alla biljetter som är äldre än 24 timmar och ta bort dem.

Ganska mycket logik för en liten funktion. Har man dessutom en klustrad miljö behöver man säkerställa att batchjobben startas på endast en nod i klustret.

Ett bättre sätt

Det vore inte så dumt om man kunde få till ovanstående utan att behöva hålla koll på tillstånd på serversidan och i databasen, och om man inte hade några som helst säkerhetsaspekter att ta hänsyn till skulle man kunna skicka en nollställningslänk som ser ut på följande sätt:

https://example.com/resetPassword?e=name@example.com&issued=201710021T10101Z

Vi skickar alltså i klartext alla parametrar, och slipper hålla koll på dem på serversidan. Det betyder ju förstås samtidigt att vem som helst som kan läsa mönstret också skulle kunna ändra på informationen och byta lösenord för vilken användare som helst, så det duger inte i verkligheten.

Hur kan man säkerställa att ingen kan ändra meddelandet? Det finns en beprövad metod för detta – en digital signatur. Om vi bakar ihop alla parametrar och signerar dem och sedan hakar på signaturen på slutet kan vi direkt detektera att meddelandet inte är manipulerat:

https://example.com/resetPassword?e=name@example.com&issued=20171002T110101Z&sign=F39F39CC0A16

Det man kan anmärka på är att det verkligen är att skjuta mygg med kanoner att använda sig av en digital signatur för att verifiera ett meddelande man själv skapat. Man ska hålla reda på både en publik och en privat nyckel, och den underliggande algoritmen är beräkningsintensiv. Borde man inte kunna göra det enklare? Jo – det finns en teknik även för detta: HMAC, eller Hashed Message Authentication Code. Lite förenklat innebär HMAC att man slår ihop ett meddelande med en hemlig nyckel och producerar en hash av totalen. Att verifiera integriteten i ett tidigare ”signerat” meddelande görs genom att räkna fram en ny HMAC och jämföra med den som följer med meddelandet. Om de är lika är innehållet heller inte manipulerat.

Hur gör vi för att förhindra att samma länk används flera gånger? Ett sätt att göra det på är att ta med användarens lösenords-hash i beräkningen av HMAC-signaturen. Om användaren redan har ändrat sitt lösenord kommer det att bli en annan HMAC vid verifieringen. Observera att man inte ska skicka med själva lösenordshashen i e-postmeddelandet.

Den nya lösningen kan nu beskrivas enligt följande:

  1. Kunden matar in sin e-postadress
  2. Kolla i databasen att e-postadressen finns registrerad
  3. Skapa en HMAC bestående av e-postadress, tidsstämpel och lösenords-hash.
  4. Skicka e-postmeddelande med en länk på formen https://example.com/resetPassword?e=name@example.com&issued=20171002110101&mac=EB9C1AD6416F3CFD139C0A1BA76E311FE

När användaren fått e-postmeddelandet och klickar på länken kommer sedan följande process att köras:

  1. Slå upp kontouppgifter baserat på e-postadress, beräkna och verifiera att man får samma HMAC som i meddelandet
  2. Kontrollera att tiden inte har passerat
  3. Erbjud användaren att mata in nytt lösenord
  4. Spara det nya lösenordet för användaren med lämplig kryptografiskt säker hashnings-algoritm

Med det är vi färdiga. Inget behov av ny information i databasen, och därför heller inget behov att rensa gamla biljetter.

Avslutande tankar

Förhoppningsvis visar exemplet ovan att det finns en del att tjäna på att använda sig av HMAC för självbetjäning kring bortglömda lösenord. Användningsområdet är dock bredare än så. Generaliserat kan man använda HMAC för att säkerställa att information som lämnats ut inte har förändrats innan den skickas tillbaka.