Skydda dina användares lösenord med hashfunktion och salt

Det är inte sällan man läser om sajter som har blivit hackade där det ofta visat sig att lösenorden varit lagrade i klartext. Förutom det uppenbara problemet med att hackaren kan logga in på den berörda tjänsten med vilken användare som helst och möjlighet att förstöra, så skapar det indirekt problem även för andra tjänster på internet. På grund av att användarna ofta använder samma lösenord för olika tjänster så kan en hacker exempelvis prova samma lösenord på andra tjänster, såsom gmail och hotmail och därmed ta över användares e-postadress också.

För att minska risken för ovanstående så skall sajtägare givetvis jobba för att få användare att ha olika lösenord. Det andra är att man som sajtägare skall ta sitt ansvar och skydda sina användares lösenord på ett bra sätt, och det är det jag skriver om i detta inlägg.

De komponenter som jag brukar förespråka skall finnas i ett bra lösenordssystem är: lösenord, hashfunktion, systemsalt, användarsalt, sammansättning, iterationer och versionshantering. Jag går igenom varje del för sig. Jag kommer i varje del beskriva varför det behövs.

Lösenord

Ditt lösenordssystem bör hjälpa användarna att välja ett bra lösenord. Sätt exempelvis krav på att lösenordet måste innehålla minst 8 tecken, varav minst en siffra och en icke alfanumerisk. Tänk på att kontrollera lösenordet både på klientsidan och serversidan. Läs gärna mitt inlägg om avancerade reguljära uttryck för att se exempel på hur det kan lösas. Kontrollera att lösenordet inte är samma som delar av e-postadress eller användarnamn.
I samband med att användaren väljer lösenord bör du visa en hjälptext för användaren hur man väljer ett bra lösenord. Hjälptexten kan exempelvis innehålla:

  • Välj ett lösenord som är svårt att gissa sig till
  • Välj ett lösenord som inte kan kopplas till dig eller någon annan familjemedlem exempelvis via namn, datum, smeknamn
  • Välj ett unikt lösenord som du inte använt på någon annan sajt

I hjälptexten bör du dock inte ge riktlinjer om vilket lösenord man skall välja, då är risken stor att användaren kommer att skapa ett lösenord efter dina riktlinjer. Hjälptexten bör alltså inte innehålla fraser som:

  • Välj ett svårt lösenord, exempelvis: Jua7;s..A’82
  • För att komma ihåg ditt lösenord, skapa en mening och ta de första bokstäverna till lösenordet, såsom. ”Jag heter John och är 90 år gammal.”, skulle då bli JhJoä90åg.

Varje del i detta inlägg kommer att utveckla lösenordssystemet och göra det säkrare. Jag vill därför via kod visa hur det kan se ut utan hashning.

password = ”Ha%Ndl3(2~1”; // 11 tecken långt lösenord
userController.SavePassword(password); // lösenordet lagras i klartext

Hashfunktion

Eftersom vi inte vill lagra lösenordet i klartext så vill vi hasha användarens lösenord innan det lagras i databasen. En hashfunktion är så kallad envägskryptering, vilket betyder att det inte går att återskapa originaldatat från den krypterade (eller hashade) strängen. Läs mitt inlägg om hur en hashfunktion fungerar om du vill veta mer.

Du bör använda en så säker hashfunktion som är möjligt i just ditt projekt. Det kan exempelvis vara sha512. md5 och sha1 anses vara osäkra i dag. Det är andra – säkrare – hashfunktioner på gång, såsom sha-3 och skein.

password = ”Ha%Ndl3(2~1”; // 11 tecken långt lösenord
hash = sha512(password); // lösenordet hashas med sha512
userController.SavePassword(hash); // lösenordet kan inte återskapas eftersom det är hashat

Systemsalt

Problemet med att enbart hasha lösenordet är att en eventuell hacker då kan skapa en databas som innehåller hashvärden av alla ord i en ordlista, och om inte det skulle räcka för att knäcka lösenord, så kan en hacker generera alla kombinationer upp till n tecken [a-zA-Z0-9] samt diverse icke alfanumeriska tecken. Det finns redan projekt för detta, exempelvis md5this eller rainbow table som inte ens behöver generera alla kombinationer.

Därför bör man lägga till ett salt som är samma för alla användare. Saltet bör vara långt, gärna över 20 tecken och ha samma krav som ett vanligt lösenord. Alltså bestå av siffror och icke alfanumeriska tecken.

Kodexempel

salt = ”PP’P0M`*4e&5I@}” // långt salt
password = ”Ha%Ndl3(2~1”; // 11 tecken långt lösenord
hash = sha512(salt + password); // skapar hash baserat på lösenord och salt
userController.SavePassword(hash); // lösenordet kan inte återskapas eftersom det är hashat

Användarsalt

Problemet med att enbart hasha och använda systemsalt är att två användare som har valt samma lösenord har samma hashvärde i databasen. Detta kan utnyttjas av en hacker som kommer åt databasen. Exempelvis genom att generera nya användare med unika lösenord. Om den nyskapade användaren har samma hashvärde som en annan användare i databasen så har man knäckt den användarens lösenord.

För att motverka den möjligheten kan vi lägga till ett användarsalt som är unikt för varje användare. Då kommer hashvärdet inte att bli samma även om två användare har valt exakt samma lösenord.

usersalt = ”<4VKfZK.^>KbT\G=sg[U^|Zz7hzfrh]vVA$ORE,b.Lbi42Gwk2flGy23PEOtB.”; // hämta användarsalt exempelvis från databasen
salt = ”PP’P0M`*4e&5I@}” // långt salt
password = ”Ha%Ndl3(2~1”; // 11 tecken långt lösenord
hash = sha512(salt + password + usersalt); // skapar hash baserat på lösenord och salt
userController.SavePassword(hash); // lösenordet kan inte återskapas eftersom det är hashat

Sammansättning

Hur man sätter samman sin hash kan variera. Anledningen till det är att om en hacker exempelvis kommer åt systemsalt och användarsalt så kan det användas för att göra en brute force. Problemet som hackern har är att han inte vet hur hashen är sammansatt. Se mitt kodexempel nedan för två exempel som kommer att resultera i två helt olika hashvärden, trots att alla komponenter systemsalt, lösenord och användarsalt är samma.

hash1 = sha512(salt + password + usersalt); // exempel på en sammansättning
hash2 = sha512(usersalt + password + salt); // exempel på en annan sammansättning
hash3 = sha512(sha512(usersalt + password) + salt); // exempel på en tredje sammansättning
hash4 = sha512(salt + usersalt + sha512(password)); // exempel på en fjärde sammansättning

I de två första sammansättningarna har jag bara valt att byta plats på systemsalt och användarsaltet. Denna enkla förändring gör det dock genast svårare för en hacker att genom brute force få fram användarens lösenord. De två nedre är exempel där jag har valt att först skapa en hash och sedan skapa mitt slutgiltiga hashvärde baserat på den hashen och de återstående värdena.

Iterationer

Om en hacker får tag i hashvärdet, användarsaltet, systemsaltet samt sammansättningen så är det givetvis mycket illa. Men för att hackern skall kunna återskapa lösenord från dessa hashvärden så måste han köra brute force som helt enkelt skapar en hash baserat på alla möjliga kombinationer. När hackern lyckas skapa en hash som motsvarar den som fanns i databasen så har han fått fram lösenordet. Datorer kan prova flera tusen olika lösenord i sekunden, naturligtvis beroende på datorkraft men också på hashfunktion och sammansättning. Man vill försöka få ner antalet lösenord som kan testas per sekund, ett sätt är att lägga till iterationer som helt enkelt hashar lösenordet flera gånger. På det sättet måste även en hacker hasha lösenordet flera gånger vilket då tar processorkraft och i slutändan tar det längre tid för en hacker att göra brute force.

usersalt = ”<4VKfZK.^>KbT\G=sg[U^|Zz7hzfrh]vVA$ORE,b.Lbi42Gwk2flGy23PEOtB.”; // hämta användarsalt exempelvis från databasen
salt = ”PP’P0M`*4e&5I@}” // långt salt
password = ”Ha%Ndl3(2~1”; // 11 tecken långt lösenord
hash = sha512(salt + password + usersalt); // skapar hash baserat på lösenord och salt
for (int i=0;i<1000;i++){ hash = sha512(hash); // skapar ny hash av det föregående hashvärdet } userController.SavePassword(hash); // lösenordet kan inte återskapas eftersom det är hashat [/sourcecode] I ovanstående exempel hashar jag hashvärdet 1000 gånger. Du kan vilja hasha fler gånger. RSA PKCS #5 rekommenderar minst 1000 iterationer.

Versionshantering

Du vill hela tiden se över ditt lösenordssystem. Kanske använder du en hashfunktion som är gammal och osäker, såsom md5. Eller så vill du ändra sammansättning, systemsalt eller iterationer. Oavsett vad så vill du kunna ändra. Därför är det bra att versionshantera dina användares lösenord. Det innebär att trots att du har bytt lösenordssystem så kan en användare logga in via det gamla systemet och när han gör det så skapar du en ny lösenordshash för användaren och nästa gång användaren loggar in görs det genom det nya lösenordssystemet. En annan fördel med detta är att du efter x dagar/veckor/månader kan stänga av alla användare som inte har uppgraderat sitt lösenord till det nya systemet, om du exempelvis har hittat en säkerhetslucka i ditt system eller om en hacker har gjort det.


passwordVersion = ”1.2”;
userController.SavePassword(hash, passwordVersion); // lösenordet sparas på samma sätt som vi gjort förut men denna gång skickar vi med versionsnummer som också lagras i databasen

Men om användaren glömt lösenordet?

En vanlig fråga som dyker upp i samband med hashning av lösenord, är hur man skall kunna skicka lösenordet till användaren om denna har glömt det. Som jag ser det så skall man aldrig skicka ett lösenord till en användare. Anledningen är lösenordet för det första skall vara hashat och för det andra är det vanligt att lösenordet skickas okrypterat via e-post som ligger lagrat i all evig framtid. Detta gör att om användarens e-post blir hackat och användaren inte har bytt lösenord så kommer hackern åt tjänsten.

Istället rekommenderar jag ett lösenordsåterställningssystem som baseras på två nycklar som är unika för varje användare. Man kan exempelvis skicka en länk (via e-post) som ser ut såhär:
restore_password.aspx?userid=1&key1=1C7D40F0-900B-11DE-AC98-81E455D89593&key2=255D0FFC-900B-11DE-938D-A5E455D89593.
Dessa två nycklar skall endast vara giltiga en viss tid, exempelvis 24 timmar samt bli ogiltiga direkt när användaren har bytt lösenord. Detta tvåkomponentsystem gör det i praktiken väldigt svårt att gissa sig till nycklarna och därmed svårt att hacka.

När användaren följer länken så kommer den till en krypterad sida (https) där han kan välja sitt nya lösenord.

5 kommentarer

  1. Intressant artikel, är ärligt talat sjukt vad dålig säkerhet det finns på många sidor. Sen ligger ju fortfarande mycket på användaren. Skrev själv om hur man kan skapa unika lösenord med hög säkerhet på ett enkelt sätt – som dessutom är lätta att komma ihåg. http://macbloggen.se/artiklar/sakert-losenord

  2. Jonas said

    Tycker artikeln är väldigt overkill. Dina argument för systemsalt uppfylls helt av användarsaltet, så systemsaltet behövs inte. Sen finns det väl ingen större nytta med att ha ett så långt salt som du har, det tillför ingenting eftersom hashfunktioner har kollisioner. Sen finns det väl ingen poäng alls att ha samma krav på skumma tecken i saltet som i lösenordet. Salt är ju till för att få variation på hasherna, så det räcker med att det är en slumpad sträng. Varken sammasättning eller iterationer tillför särskilt mycket i styrka, själva styrkan sitter i hashfunktionen.

    Det räcker gott och väl med ett slumpat användarsalt på ca 20 tecken.

    • patrik said

      Jag har valt att dela upp det i den ordning jag anser vara viktigast. Möjligen kan man byta plats på användarsalt och systemsalt, precis som du skriver.

      Anledningen till att jag inte tycker att användarsalt räcker är att systemsalt tillför en säkerhetsdimension till. Vi kan exempelvis tänka oss att en applikation är sårbar för sql-injection, om användarsaltet då finns lagrat i databasen på samma sätt som de hashade lösenorden så gäller det för hackern att bara lista ut sammansättningen för att knäcka alla lösenord. Om man däremot använder ett systemsalt så måste hackern även komma åt applikationsfilerna där systemsaltet finns.
      Angående längden på salten så har jag slumpat fram dessa. Jag ser ingen anledning att spara tecken på saltlängden, men däremot inte överdriva onödigt mycket heller. Om vi återigen tar scenariot med sql-injection så har användaren lösenordshashen, användarsaltet och behöver nu bara systemsaltet. Då vill jag vara säker på att det är svårt för hacken att genom brute-force få fram systemsaltet. För att vara säker så väljer jag att ha långa salt. 20 tecken som du nämner tycker jag dock är tillräckligt – just nu -, men då datorer bara blir snabbare så gör det ingenting om man går upp mot 30-40 tecken. Lagring är billigt så det lär knappast vara ett problem. Så varför chansa? Sammansättning tycker jag är en liten detalj, inte superviktig, som försvårar för en hacker. Iterationer är viktigt för att – som jag skriver – försvåra för hackern att få fram alla lösenord även om den har viss eller all information.
      Angående kollisioner så håller jag med, det är viktigt att man har en stark hashfunktion, det är därför jag gärna ser att man har versionshantering så det är enkelt att byta hashfunktion när den tidigare man använde blivit knäckt.

      • nitro2k01 said

        Hur osäkert är egentligen md5 om man saltar rätt?
        md5+ordbokslösenord+inget salt = knäckning på nolltid
        md5+kort lösenord (<=8 bytes) = Knäckning på en dag
        md5+systemsalt-usersalt = Alla användare med samma lösen får samma hash. (Potentiell risk om det finns andra osäkerheter)

        Så långt är jag med. Men om man har ett lagom långt systemsalt och ett lagom långt användarsalt, vad gör kollisioner för skillnad? För att kollisionen ska bli en säkerhetsrisk måste man ju även saltet stämma in för att det ska bli en risk, eller?

        Alltså md5(syssalt+usersalt+realpass) = md5(syssalt+usersalt+fakepass). Att bara veta en revers till hashen lär väl som sagt inte hjälpa inkräkare i det fallet? Eller missar jag något om exakt hur stor del av md5 som har knäckts?

      • patrik said

        Du har helt rätt. Kollisioner gäller bara för annan typ av data, och eftersom hashen skapas av två andra värden som en eventuell hacker inte har åtkomst till så blir det mindre sårbart. Givetvis förutsatt att användarsaltet och systemsaltet är tillräckligt långa. Men det betyder som sagt inte att md5 är bra och att man skall använda det. Säkerhet skall byggas rakt genom hela applikationen och alla komponeterna skall säkras. Därför förespråkar jag att man skall välja en säker hashfunktion framför en som man vet är sämre.

RSS feed for comments on this post

Kommentarer inaktiverade.

%d bloggare gillar detta: