Hur lagrar man användarnas lösenord säkert?
Publicerat 9 mars 2009 av Christian
Det skrivs ibland om webbtjänster som blir hackade och lösenord som läcker ut. Då är det en fördel om lösenorden är skyddade på något sätt, men så är det inte alltid. Hur kan man då göra för att säkra lösenorden för den händelse att ens tjänst skulle bli hackad? Jag har definierat tre säkerhetsnivåer nedan.
Nivå ”usel”: Klartext
Jag tror att de flesta webbtjänster lagrar användarnas lösenord i klartext, möjligen krypterat med en nyckel eller chiffrerat med en egen funktion eller ROT13. Alla tjänster som kan skicka ut bortglömda lösenord faller i den här kategorin. Det är inte bra, eftersom alla lösenord är tillgängliga för hackaren.
(Använder man en nyckel för tvåvägskryptering kan en hackare förstås hitta den nyckeln också, och då blir samtliga lösenord helt plötsligt läsbara i klartext.)
Nivå ”ok”: Hash
Ett snäpp bättre är att köra lösenorden genom en envägskryptering (kallas hashfunktion) och bara lagra resultatet. Populära funktioner är MD5 och SHA-1. När en användare loggar in, hashar man det angivna lösenordet och jämför med hashen i databasen för att verifiera.
Teorin bakom en hashfunktion är att det ska vara omöjligt att härleda lösenordet utifrån hashen. Även om någon kommer över en databas full med hashar, känner man inte till ett enda lösenord. För att knäcka en hash krävs det att man genererar hashar för alla tänkbara lösenord och ser vilka som matchar. Det tar lång tid, vilket är resonemanget bakom all kryptering.
Men eftersom en hashfunktion inte tar en nyckel som argument, får alla webbtjänster i hela världen alltid samma hash från samma lösenord (om man använder samma funktion, och jag tror att de flesta gör det). Det har gjort att hackare har investerat tid i att skapa uppslagstabeller för hashar. De kör helt enkelt alla tänkbara strängar genom en hashfunktion, och helt plötsligt kan de översätta ”9726255eec083aa56dc0449a21b33190″ till ”money” och så har de knäckt alla svaga lösenord. Läs mer om så kallade rainbow tables på Wikipedia.
(Starka lösenord är dock relativt svårt att komma åt om de är hashade. Det finns betydligt fler starka än svaga lösenord, så hackarna har kanske inte hunnit generera hashar för alla starka lösenord.)
Du bör förresten undvika funktionen MD5, eftersom det finns kända sårbarheter i den. Om du skapar en ny tjänst ser jag ingen anledning till att inte använda SHA-256 eller SHA-512, som är nyare och starkare varianter av SHA-1. (SHA-algoritmerna är framtagna av amerikanernas motsvarighet till FRA: NSA.)
Nivå ”utmärkt”: Saltat hash
För att göra det omöjligt att använda uppslagstabeller för hasharna, kan man enkelt lägga till ett ”salt” till lösenordet innan man kör det genom hash-funktionen. När man sedan ska verifiera inloggning, använder man samma salt och jämför resultaten. (Ett salt är vanligtvis en sträng.)
Hashen blir då beroende av saltet, och uppslagstabellerna måste göras om för varje salt. För att förtydliga, visar jag här en hash med MD5 med olika salt:
| Lösenord | Salt | Hash |
|---|---|---|
| money | inget | 9726255eec083aa56dc0449a21b33190 |
| money | salt | 36953a1b0dc73ebbcc992497d6bb07bc |
| money | NaCl | 45337febad40d0d3fb4571771f07b034 |
Som du ser blir hasharna helt olika. Du bör därför använda ett unikt salt för att dina hashar ska vara unika och svårare att knäcka.
Det bästa är att skapa ett slumpmässigt salt för varje användare, och lagra detta i databasen. (Nej, du behöver inte kryptera saltet.) Det är inte lika bra att använda samma salt för alla användare, men i alla fall bättre än att inte salta alls.
Uppdaterat: Observera att saltet måste vara slumpmässigt och av en viss längd. Om du använder salt som bara är ett par tecken långt, är det fortfarande ganska enkelt att knäcka med rainbow tables. Saltet bör vara minst 15 tecken långt enligt Dilbert-principen ”I didn’t have any accurate numbers, so I made up this one”.
Källkod (PHP)
Så, hur gör man nu? För att hjälpa dig på traven ska du få se min egen källkod, som jag använder i ett projekt. Där skapar jag ett slumpmässigt salt (av slumpmässig längd) för varje ny användare och lagrar det i en kolumn i användar-tabellen. Jag har dessutom ett statiskt salt, som är slumpmässigt genererat en gång för alla. Jag använder hash-funktionen SHA-256, som finns i modulen mhash för PHP (som tyvärr inte är installerad som standard).
För att skapa ett slumpmässigt salt:
function makeRandomString($minLength, $maxLength)
{
$length = mt_rand($minLength, $maxLength);
$string = "";
for($i = 0; $i < $length; $i++)
$string .= chr(mt_rand(32, 126));
return $string;
}
Och för att hasha lösenordet:
function hashPassword($password, $salt = "")
{
return base64_encode(mhash(MHASH_SHA256,
$salt.$password.'HJ"UPHNtfU`__HEtX;#G@4'));
}
Strängen som står efter $password är alltså det statiska saltet, och du bör generera ett eget, eller möjligen strunta i det statiska saltet. Jag är osäker på vad det egentligen tillför i säkerhet.
Om du inte har tillgång till modulen mhash, kan du istället använda den inbyggda funktionen sha1 på samma sätt:
return sha1($salt.$password.'HJ"UPHNtfU`__HEtX;#G@4');
Du kanske också har stöd för funktionen hash, då kan du skriva:
return hash('sha256', $salt.$password.'HJ"UPHNtfU`__HEtX;#G@4');
Läs också de artiklar som inspirerade denna:
- Spotify säkerhetsproblem + Hash på KTH
- 26a8012b7f9b9c441bcca35eeec32f18 (ja, artikeln heter faktiskt så)
Uppdatering: Läs denna avancerade artikel för ännu mer information: Enough With The Rainbow Tables: What You Need To Know About Secure Password Schemes. Han ondgör sig bland annat över SHA-algoritmerna, som är för snabba. Ju snabbare algoritm, desto snabbare går det ju att knäcka.



Tack för en mycket bra genomgång av detta! Detta är ju så viktigt och det är ju skam om man står där en dag och har blivit hackad. Jag har alltid detta i åtanke numera och är stenhård med detta mot utvecklare.
Väl skrivet! Tänkte bara tipsa om en aningen äldre genomgång borta på grafiskt forum som berör samma ämne. Tack vare denna tråd lärde mig salta och hasha.
http://www.grafisktforum.org/showthread.php?t=22562
Tack för en bra genomgång.
Jag har aldrig tänkt på att använt ett slumpmässigt salt och lagra det i användartabellen.
Men när jag funderar lite på det så tänker jag så här: Risken vi vill skydda oss mot är väl primärt att någon kommer över vår användartabell? Om tabellen också innehåller saltet, som väl måste vara hemligt för att fylla en funktion, har vi inte i så fall gett bort vår hemlighet?
Jag har ingen aning om hur man skulle gå till väga för at knäcka ett hash så jag bara gissar…
Jan: nej, saltet behöver inte vara hemligt. Visserligen är det (i teorin) då hyfsat lätt att knäcka ett enstaka, utvalt lösenord, men praktiskt taget omöjligt att knäcka alla lösenord samtidigt.
Du måste ju skapa en ny rainbow table för varje salt, eftersom det förändrar hashen. Och om du har ett unikt salt för varje användare, betyder det att du måste skapa lika många rainbow tables som det finns användare. Det är knappast genomförbart!
Är tämligen säker på att ett statiskt salt tillför extremt lite säkerhet om du har ett bra dynamiskt salt. Själv brukar jag köra:
$salt = md5(mt_rand());
Kan förresten vara bra att påpeka att valideringen av en användares lösenord inte ska ske genom en databasfråga, då du i så fall kan läsa av loggen för att eventuellt få ut ett lösenord i klartext. Exempelvis: … WHERE `password` = SHA1(CONCAT(`salt`,’mitt_lösenord’));
Kan tänka mig att det statiska saltet gör så att det blir lite svårare om man bara har hasharna och inte php filerna. Som vid sql injection t.ex.
Tack du har hjälpt mig förstå rainbow tabels bättre.
Tack för en bra artikel. Det här kommer garanterat till användning i mitt nästa PHP-projekt. (Som självklart inte kommer ”gå live” eftersom det lär vara fyllt med hål)