Tak, znowu będzie o hasłach. Tym razem o przechowywaniu haseł. A bezpośrednią inspiracją jest (tak, zgadliście) to: Allegro: kontrowersje wokół sposobu przechowywania haseł, ale od razu uprzedzam - na ten temat nie napiszę nic.
O przechowywaniu haseł
Krótkie wprowadzenie
Na początek przypomnienie pewnego schematu:

Hasła są przechowywane w data store określone jako dane. Powinny być przechowywane tylko tam, pomijając oczywiście ewentualne zapisanie hasła przez użytkownika (przypominam, że przeszedłem na KeePass jakiś czas temu).
Intruz w posiadanie haseł przechowywanych w bazie może wejść w co najmniej kilka sposobów. Mnie kilka razy w trakcie testów udało się to osiągnąć przez sql injection, ale równie dobrze można sobie wyobrazić, że w ręce intruza trafia dysk, który po dożyciu swych lat przewidzianych zamiast do utylizacji, trafił na aukcję. W końcu kryzys jest i skoro na starym sprzęcie można zarobić, to czemu tego nie robić (a tak przynajmniej niektórzy myślą). Sposób w jaki intruz wejdzie w posiadanie bazy haseł nie jest jednak istotny, ważne jest natomiast to, co może zrobić z tymi danymi. A to, co może zrobić z tymi danymi zależy od tego w jakiej postaci przechowywane są hasła. Dla uproszczenia przyjmijmy, że przypadki mogą być dwa:
- hasło jest przechowywane w formie jawnej (clear text),
- hasło jest przechowywane w formie niejawnej
Jest baza, co dalej
Hasła w postaci jawnej
Jeśli hasła przechowywane są w formie jawnej, to dalej jest płacz i zgrzytanie zębami. Intruz (prawdopodobnie) dysponuje wówczas wszystkimi danymi potrzebnymi do uwierzytelnienia, może działać w kontekście dowolnego użytkownika, który ma nieszczęście znajdować się w tej bazie. Zakres działania intruza nie jest ograniczony tylko do tego serwisu, który padł jego łupem. Dlatego korzystanie z jednego hasła w wielu różnych serwisach nie jest najlepszym pomysłem. Wystarczy by jeden z serwisów stracił bazę haseł, konta w pozostałych serwisach, nie ważne jak dobrze i bezpiecznie serwisy te przechowują hasła, też mogą się stać "ofiarą".
Hasła w postaci niejawnej
Zdecydowanie ciekawszym przypadkiem jest ten, w którym hasła przechowywane są w postaci niejawnej.
Hasła w postaci odwracalnej (zamierzenie lub nie)
Pierwsze pytanie - czy funkcja wykorzystywana do przechowywania haseł jest (lub czy może być) odwracalna. O ile nie ma takiej potrzeby - nie powinna być. Dla przypomnienia dwa przykłady, gdy teoretycznie zabezpieczone hasła w praktyce przechowywane są praktycznie w formie jawnej:
Jeśli baza haseł (lub poszczególne hasła) są szyfrowane "dobrym" algorytmem, trzeba pamiętać jeszcze, że Encrypted data is only as secure as the decryption key. Czyli mówiąc wprost zawarcie w tej samej bazie zaszyfrowanych haseł oraz klucza kryptograficznego użytego do szyfrowania byłoby spektakularnym przykładem głupoty.
Hashe (wynik funkcji jednokierunkowej)
Przekształcenie hasło -> hash powinno być jednokierunkowe. Teoretycznie wymaganie to spełnia każda funkcja hashująca (np. MD5, SHA1), w praktyce bezpośrednie wykorzystanie takich funkcji nie jest rozwiązaniem poprawnym.
Skoro nie istnieje przekształcenie pozwalające na uzyskanie z hasha oryginalnego hasła, działanie intruza musi opierać się na sprawdzeniu, jakie hasło daje określony hash. Pomijając atak na sam algorytm hashowania, może to zrobić na dwa główne sposoby:
- atak słownikowy,
- atak brute force,
Krytyczną dla takiego ataku kwestią jest to, ile haseł w jednostce czasu będzie mógł sprawdzić atakujący. Im więcej, tym lepiej dla niego, zwłaszcza jeśli atak słownikowy nie przynosi oczekiwanych efektów i konieczny jest brute force. Temat wydłużania czasu potrzebnego na sprawdzenie poprawności hasła zresztą już poruszałem, patrz: Bootcamp XI: opóźnienia przy logowaniu, tu jednak intruz ma bezpośredni dostęp do hasha hasła, więc pokazywane w tym przykładzie sposoby na spowolnienie brute force nie są skuteczne.
To ile haseł intruz będzie w stanie sprawdzić w jednostce czasu zależy bezpośrednio od tego, w jaki sposób hasła są przechowywane, czyli od tego jak generowany jest hash. Przy okazji warto jest mieć na uwadze kilka dodatkowych kwestii:
- algorytmy hashowania są z reguły stosunkowo szybkie co oznacza, że przekształcenie hasła na hash (MD5, SHA1) nie operacją szczególnie kosztowna obliczeniową,
- do liczenia hashy można wykorzystać specjalizowany sprzęt, który jest powszechnie dostępny - mowa o kartach graficznych, których GPU w operacjach kryptograficznych może być wielokrotnie bardziej wydajne, niż zwykłe CPU (muszę w końcu się zebrać w sobie i przetestować łamanie haseł z wykorzystaniem CUDA: Holy Crack!,
- wyliczone hashe można wykorzystać wielokrotnie (patrz: rainbow tables) ponieważ przekształcenie hasła na hash z wykorzystaniem funkcji takich jak MD5 czy SHA1 jest jednoznaczne, to samo hasło daje zawsze ten sam hash,
Z rainbow tables poradzić sobie jest stosunkowo łatwo - wystarczy wprowadzić unikalny dla każdego hasła salt. Wówczas wynik operacji hash(salt, haslo) jest zależny nie tylko od hasła, ale również od owej (losowej i długiej) wartości salt. Salt oczywiście musi być przechowywany w bazie po to, by móc zweryfikować poprawność hasła, tak więc jego zastosowanie nie wnosi praktycznie żadnej wartości dodanej w odniesieniu od ataku słownikowego lub brute force.
Sposób zwiększenia kosztu obliczeniowego sprawdzenia poprawności hasła jest również stosunkowo prosty. Można na przykład operację hash(salt, haslo) wykonać wielokrotnie, przy czym w kroku n+1 jako haslo należy wykorzystać wynik uzyskany w kroku n. Ilość iteracji może być wartością określoną parametrami systemu. Przy tak realizowanej funkcji wyliczania hasha hasła postęp w zakresie wydajności komputerów może być kompensowany przez zwiększenie ilości iteracji.
I na koniec podsumowanie
Przy składowaniu haseł trzeba pamiętać, że:
- przechowywanie hasła w postaci jawnej jest ZŁE!,
- wykorzystywanie własnej roboty algorytmów kryptograficznych jest ZŁE!,
- przechowywanie haseł w postaci hashy (np. MD5, SHA1) też jest ZŁE!,
- dla każdego hasła przed operacją wygenerowania hasha należy wygenerować losowy, unikalny salt,
- operacja wyliczania hasha powinna być kosztowna obliczeniowo,
- kosztowność obliczeniową można zwiększyć przez zwiększenie ilości rund algorytmu,
Przykładem algorytmu przechowywania haseł, który właśnie w ten sposób działa jest blowfish crypt.
W przypadku aplikacji internetowych warto się zastanowić, czy implementowanie haseł i ich przechowywanie jest potrzebne: Jak rozsupłać hasło, czyli alternatywne przemyślenia o haseł implementacji?
A także:
Ochrona i łamanie haseł w aplikacjach, Bezpieczne hasła w XXI wieku?
http://www.securitystandard.pl/artykuly/56901_0/Ochrona.i.lamanie.hasel.w.aplikacjach.html
http://www.securitystandard.pl/artykuly/347006/Bezpieczne.hasla.w.XXI.wieku.html
Oczywiście na żywo byłoby lepiej.