Az eddigi ismereteink alapján jó ötletnek tűnhet a felhasználók jelszavait hash formában tárolni. Azonban ez a módszer nem nyújt megfelelő biztonságot. Mégpedig azért nem, mert a számítógépek hardverei igencsak fejlődnek és a korábban lehetetlennek tűnő tárkapacitások is mára elérhetővé váltak és megjelentek a rainbow table alapú támadások.
De mi is egy rainbow table? Lényegében egy inverz hash táblázat, ahol a hash algoritmus kimenetből megmondjuk a bemenetet. Matematikailag még mindig igaz, hogy a hash egyirányú művelet, de semmi nem akadályoz meg bennünket abban, hogy ezt az egyirányú folyamatot felhasználva jelszavakat hasheljünk, majd a hash alapján kereshetővé tegyük őket.
Tételezzük fel, hogy az algoritmus, amit használunk (SHA1) 160 bites. Ha minden hash értéket le szeretnénk tárolni, akkor 2168 bejegyzésünk lenne, ahol csak a hash érékek tárolása ~2,59 x 1034 TiB nagyságrendű adatot jelentene.
Ez azonban egy nagyon pesszimista becslés. A felhasználók nem szokták túlkomplikálni a jelszóválasztást, éppen ezért bőven kevesebb hash tárolásával is igen komoly eredmények érhetőek el.
Nézzünk egy lehetséges példát. Tételezzük fel, hogy a felhasználónk nem bonyolította túl a dolgokat és a jelszava az abcd1234. Ebben az esetben az SHA1 étéke a jelszónak 7ce0359f12857f2a90c7de465f40a95f01cb5da9.
A felhasználói adatbázisunkat ellopták, így a támadó a táblában az 7ce0359f12857f2a90c7de465f40a95f01cb5da9 értéket látja, amire rákeresve egy rainbow táblázatban egyből rá tud jönni, hogy a jelszó abcd1234 volt. Ezt azért tudta megtenni, mivel a bemeneti jelszó gyenge volt. A nagyobb probléma azonban az, hogy a felhasználó valószínűleg ezt a jelszót máshol is használta, ha pedig az e-mail fiókjához is ugyanez a jelszava, akkor bizony igen nagy bajba kerülhet csupán egyetlenegy adatlopás által.
Éppen ezért manapság a jelszavak esetén nem csak a hash tárolt. Ugyanúgy hash értéket tárolunk, de a bemenethez fűzünk egy véletlenszerűen generált értéket és ezt tároljuk le.
Ezt a folyamatot nevezzük salting-nak, vagyis sózásnak. A szimpla sózás picit biztonságosabb, mint a sima hash értékek tárolása, azonban gyengesége az algoritmusnak, hogy ha a salt érték ismert, akkor a hash többi része brute force próbálkozással ugyanúgy törhető. Ez nem feltétlen probléma, azonban érdemes észben tartani, hogy a salt hozzáadás önmagában csak a rainbow table készítést, használatot akadályozza meg.
Mivel sózni sokféleképpen lehet az alap hash értéket, elkerülhetetlen, hogy legyenek jobb és rosszabb megoldások. A rosszabb megoldások kivédésére született meg a PBKDF2 (Password based key derivation function 2) algoritmus, ami RFC számot is kapott, mégpedig a 2898-at. (https://www.ietf.org/rfc/rfc2898.txt)
Ez a megoldás N alkalommal sózza és újra hasheli a bemenetet, ami végső soron azt jelenti, hogy a brute force alapú támadást megnehezíti, de nem lehetetleníti el!
A PBKDF2 a modern GPU-k megjelenésig biztonságosnak számított, de az algoritmus kis memória használatának köszönhetően könnyen implementálható GPU-n. Egy modern GPU számítási teljesítménye bőven a több Teraflops1 kategóriában mozog, éppen ezért az ilyen és ehhez hasonló cél áramköröket használó törésekkel szemben nem nyújt sok védelmet.
Éppen ezért ha ilyen törésekkel szemben is védekezni szeretnénk, akkor a PBKDF helyett a Scrypt algoritmus használata ajánlott, ami szintén kapott RFC számot, mégpedig a 7914-et. (https://www.rfc-editor.org/rfc/rfc7914.txt)
A Scrypt úgy védekezik a GPU és cél áramkör (ASIC) alapú törések ellen, hogy a számítási igény mellett a memóriaigényt is növeli. A memóriaigény azért fontos, mert limitálja az egyszerre párhuzamosan futtatható töréseket, illetve a memória elérés bármelyik gépen nagyságrendileg lassabb, mint a processzor regisztereiben tárolt adatok elérése.
A .NET keretrendszer jelen pillanatban csak PBKDF2 alpú sózással rendelkezik. Ezt a Rfc2898DeriveBytes osztály valósítja meg. Az osztály többféle konstruktorral is rendelkezik, de általában a háromparaméteres változatát szokás használni:
Rfc2898DeriveBytes(byte[] password, byte[] salt, int iterations);
Az első paraméter a jelszó byte reprezentációja. Ezt könnyen megszerezhetjük a System.Text.Encoding osztály megfelelő kódolását használva egy szövegből. A második paraméter a sózáshoz használt byte tömb. Ennek minimum 8 byte méretűnek kell lennie. A harmadik paraméter pedig az iterációk száma. Az iterációk számának növelése növeli az algoritmus futási idejét és a biztonságát. Ennek a minimuma 1. Ha a PBKDF2-t tényleg biztonságosan szeretnénk használni, akkor legalább 1300000 iterációt alkalmazzunk, mivel az alapértelmezetten használt Hash algoritmusa az SHA1.
Ha nem SHA1 algoritmust szeretnénk használni, akkor a négyparaméteres konstruktorát kell használni, amiből a negyedik a Hash algoritmus neve:
Rfc2898DeriveBytes(byte[] password, byte[] salt, int iterations, HashAlgorithmName hashAlgorithm);
Az alábbi táblázat az OWASP2 által ajánlott iteráció számokat foglalja össze különböző algoritmusok esetén:
| Algoritmus | Iterációk száma |
|---|---|
| MD5 | Nem ajánlott használni |
| SHA1 | 1 300 000 |
| SHA2 256 | 600 000 |
| SHA2 512 | 210 000 |
A példányosítás után a GetBytes(int cb) metódus meghívásával tudunk a paraméter által meghatározott byte-hoz jutni, amit biztonsággal használhatunk a titkosítás során kulcsként.
-
Egy másodperc alatt elvégezhető lebegőpontos műveletek számát jelöli a flops. A Teraflops ebből adódóan 1012 nagyságrendet jelöl. Egy GeForce RTX 3060 esetén ez 12,74 Teraflops 32 bites lebegőpontos számokkal, 64 bites pontosság esetén pedig 199 Gigaflops↩
-
https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html↩