Spara lösenord säkert med hash+salt

Det har varit väldigt mycket liv kring hur man ska spara lösenord den senaste tiden. Det är knappast någon som missat att Bloggtoppen blev hackat och att Aftonbladet lyfte fram detta som enorm händelse. Eftersom detta är högst aktuellt även om jag borde skrivit detta i förra veckan så tänkte jag passa på att gå in på säkerheten och hur du på bästa sätt kan spara användares lösenord i databasen. Mitt exempel kommer varit skrivet i PHP men samma princip gäller för vilket program/script-språk som helst.

Innan jag går in på min lösning så tänkte jag gå igenom säkerhetsproblemen som uppstår och varför de är dåliga. Jag är absolut ingen säkerhetsexpert på ämnet men jag känner ändå att jag har relativt bra koll på läget och en bra lösning på säkerhetsproblemen. Kanske läser någon med bra säkerhetskunskaper detta och kan ge feedback på min lösning

Säkerhetsproblemet med Bloggtoppen

Problemet med Bloggtoppen och hur de bevarade lösenorden var att de endast körde en s.k. MD5 hash på lösenorden och utan att använda sig av någon salt för att försvåra framtagandet av lösenorden. MD5 är en kryptografisk hashfunktion som är framtagen 1991 av Ronald Rivest. Den skapar en 128bit hashsträng av en valfri sträng och den gör det också väldigt snabbt. Och snabbt är i detta fallemer på ont än på gott.

Bloggtoppen använder sig av ett topplist-script vid namn evoTopsites om jag inte är helt ute och cyklar. Ett script som inte längre uppdateras eller har en officiell hemsida. Det är gammalt och likt många gamla script så använder det endast en md5-hash för att lagra lösenorden. Idag är en md5-hash utan salt knappt bättre än att lagra lösenorden i klartext. Kombinerat med ett bra salt-värde så är det helt okej även om det finns bättre lösningar.

Eftersom MD5 är så snabbt och eftersom det använts av så många gamla script så finns det idag extremt stora rainbow tables för md5-hashes. Detta gör att många lösenord sparade med MD5-hash knäcks direkt om de kommer ut i orätta händer just för att man på förhand redan skapat miljontals md5-hashar att jämföra lösenorden mot.

Rainbow tables och Brute force

Rainbow tables är tabeller som innehåller färdiga hash-värden. Exempelvis skulle man kunna gå igenom alla tänkbara kombinationer av lösenord upp till 10 tecken och spara alla dessa hash-värden i en databas. När man sedan får tag i en MD5-hash så kör man den mot databasen för att se om man hittar samma hash. Om lösenordets längd tillsammans med saltets längd inte är över 10 tecken så kommer den att få en träff i databasen och ditt lösenord knäcktes på bara några minuter. Om webbplatsen inte använt sig av salt för att göra lösenorden längre så spelar det ingen roll vilka tecken du kombinerat om ditt lösenord är 10 tecken eller mindre. Det är därför viktigt att ha ett unikt salt värde att förlänga lösenorden med så att rainbow tables inte fungerar.

Brute force innebär att man skickar påhittade lösenord tills man knäcker lösenordet. Detta kan man göra genom att skicka inloggningsförfrågningar direkt mot hemsidan. Om inte webbplatsen kontrollerar antalet felaktiga inloggningar och blockerar för många felaktiga försök så spelar det ingen roll hur bra säkerheten är i övrigt. Då hänger det helt och hållet på hur bra lösenordet är. Men eftersom du som webbplatsägare har säkerheten i fokus så ser du till så att det inte är möjligt för den som attackerar att kunna göra så många felaktiga inloggningar utan att låsas ute.

För att lyckas köra en brute force attack där det inte går att göra för många felaktiga inloggningar på rad så krävs det att man har tillgång till hashen av lösenordet och funktionen som generar hashen. Men då krävs det att man får tillgång till både databasinformation och programkod. I ett sådant scenario finns några säkerhetsåtgärder du kan göra på förhand. Dels kan du tvinga användarna att ha ett svårt lösenord att gissa sig till vilket gör att själva brute force-metoden tar lång tid. Det andra är att se till det tar tid att generera hashen vilket förlänger brute force-attacken ytterligare.

Min hash-funktion för lösenord med PHP

Jag vill börja med att säga att jag använder inte exakt samma funktion och om du vill använda min funktion så rekommenderar jag att du ändrar lite på den så att den blir unik för dig.

Mitt mål med hash-funktionen var följande:

  • Använda en hash-algoritm som är bättre än MD5.
  • Inparametrar för lösenord och salt.
  • Skapa ett unikt saltvärde baserat på en del av lösenordet och det inmatade saltvärdet.
  • Göra funktionen långsammare för att försvåra brute force attacker.
  • Tvinga fram en minimumlängd som den slutgiltiga hashen baseras på.
  • Påtvinga att specialtecken används i den slutgiltiga hashen.

Jag tycker jag har lyckats väldigt bra och jag hoppas inte jag missat något uppenbart som gör den värdelös. Som hash-algoritm använder jag mig whirlpool som standard. Whirlpool genererar en 512bit hash-sträng och har ännu ingen känd brist vad jag kunnat hitta. Om man vill kan man använda sig av någon annan algoritm så som SHA-512 eller SHA-256. Jag har gjort det enkelt genom att ta med en inparameter för detta.

För att göra funktionen långsammare så generar jag en salt-sträng med hjälp av det inmatade saltvärdet + de tre sista tecknen i lösenordet (om lösenordet är mindre än tre tecken används en förbestämd sträng vilket aldrig ska ske ändå.). Dessa ingår i en loop som körs 500 gånger i exemplet för att ta fram en sträng som används som salt. Tanken här är att göra funktionen långsammare och som bonus skapar jag ett dynamiskt saltvärde som baseras på lösenordet som ingen mer än användaren känner till. Räknat lågt på 50 olika tänkbara tecken så ger det 125 000 (50³) möjliga saltvärden. Om någon mot förmodan skulle få tillgång till både källkod och hashsträngarna för lösenorden, och försöka köra en brute-force så tvingar jag dem att köra loopen som gör hash-funktionen långsammare. Alternativt får de prova varje lösenord med 125 000 olika salter vilket också gör det mycket långsammare. Oavsett har jag försvårat för dem.

Jag avslutar sedan funktionen genom att dela på det genererade saltet och lösenordet så att jag får totalt 4 olika strängar. Jag hashar sedan de delade lösenorden med det delade saltet och kombinerar sedan ihop alla strängarna + det inmatade saltet och ett par specialtecken som jag hårdkodat i funktionen för att generera den slutgiltiga hash-strängen med whirlpool.

Genom att ändra antal, hash-algoritm, antal loopar och hur de kombineras ihop så kan du skapa en egen unik funktion baserat på min kod.

<?php
/**
* Function to generate a unique hash-tag with whirlpool 512bit (128char long) as standard algorithm.
* Looping through 500 hashes with whirlpool (std) to slow down the process and get a unique salt value.
*/
function password_hash($password, $salt, $algo='whirlpool') {
// Generate a unique salt string to slow down the process
$new_salt = $salt;
for($i=0;$i<500;$i++)
$new_salt = hash($algo, ($salt.((strlen($password)>3)?substr($password,-3,3):'U9#').$new_salt));

// Use the first 30 characters as two different salts
$first_salt = substr($new_salt,0,20);
$second_salt = substr($new_salt,20,10);

// Split the password into two parts
$password_parts = str_split($password, (strlen($password)/2)+1);

// Generate hashes of the split passwords
$password_parts[0] = hash($algo, $first_salt.$password_parts[0]);
$password_parts[1] = hash($algo, $second_salt.$password_parts[1]);

// Create the password hash to use
$hashcode = hash($algo, $salt.'#'.$first_salt.$password_parts[0].$second_salt.'&s!'.$password_parts[1]);

return $hashcode;
}
?>

Hur säker är min hash-funktion?

Jag skulle vilja tro att min hash-funktion är väldigt säker, men jag hade gärna fått det bekräftat av en säkerhetsexpert. Eventuellt skulle man kanske använda mer än 3 tecken för att skapa fler tänkbara salt-alternativ. Oavsett vilken metod du använder dig av för att hasha lösenord så måste du ställa krav på användaren. Du kan inte låta dina användare använda lösenord som ”hej” exempelvis. Det skulle inte ta många sekunder att knäcka med brute force trots att jag gjort funktionen nästan 500 gånger långsammare.

På min utvecklingsserver tar det ca 0.00001 sekunder att generera en hashsträng med whirlpool. Min funktion exekveras på ca 0.0059 sekunder och istället för att kunna skapa 100,000 hashsträngar per sekund så kan man ”endast” skapa ungefär 168st per sekund med min funktion. Men då får man ha i åtanke att tiderna avser PHP-script och inte ett program skrivet i C. Processorn är också några år gammal och en modernare processor gör det mycket snabbare än så. Men den mest väsentliga skillnaden är att man kan använda sig av grafikkortens GPU då de är mångdubbelt snabbare än processorn på detta. Så med bra utrustning så är det betydligt fler än 168st per sekund.

För skoj skull kan vi räkna med att man genererar hash-strängar 100 gånger snabbare än min utvecklingsserver. Räknat på mitt tidigare exempel om att det endast finns 50 unika olika tänkbara tecken att använda sig av så skulle det se ut enligt följande tabell:

Lösenordslängd Maximal tid för brute force
4 tecken 6min 12sec
5 tecken 5h 10min
6 tecken 10d 18h
7 tecken 1y 173d
8 tecken 73y 266d

Någonting mindre än 7 tecken känner jag direkt är olämpligt. Men eftersom datorkraften hela tiden blir bättre hade jag nog krävt 8 tecken med variation av olika tecken för att kunna skydda användarnas lösenord så gott som möjligt. Men hur långt får man egentligen gå och när börjar det bli en viktig fråga om användarvänlighet?