Ochrana hesla hashom a custom autentifikácia
V príspevku s veľavravným názvom :-) Custom authentication sme si ukázali, ako jednoducho sa dá nahradiť Provider funkcionalita pri forms autentifikácii niečím efektívnejším a prispôsobiteľnejším. MembershipProvider však ponúka oveľa viac funkcionality. Ponúka napr. možnosť zvoliť si úroveň ochrany uloženého hesla. K dispozícii máme ochranu symetrickou šifrou alebo ukladanie hesla v salt hashovanej podobe. Takúto ochranu by sme radi asi doplnili do našej aplikácie.
V krátkosti si povedzme o zmysle a výhodách jednotlivých prístupov.
Symetrická šifra
Ukladanie hesla zakódovaného symetrickou šifrou je miernym vylepšením bezpečnosti oproti ukladaniu v čistej podobe. Ak sa útočník dostane ku databáze používateľov s heslami, musí získať ešte klúč, prostredníctvom ktorého sa heslá zakryptovali. Ak uložíme kľúč niekam na suborový systém a navyše iný počítať, ako je umiestnená databáza, nastavíme striktne oprávanenia na súborovom systéme nad týmto súborom, alebo ho inak bezpečne a oddelene uložíme, značne skomplikujeme rekonštrukciu hesiel. Útočník musí získať aj onen kľúč. Určitou výhodou prístupu môže byť fakt, že heslo sa dá spätne dekryptovať a ak existuje bezpečný kanál pre jeho doručenie, vieme mu umožniť prihlásenie bez komplikácií.
Hashový algoritmus
Hash algoritmus je algoritmom tranformujúcim reťazec ľubovoľnej dĺžky na iný reťazec konštatnej dĺžky tak, aby nebolo možné spätne rekonštruovať pôvodný reťazec a aby existovala vysoká entropia výsledných reťazcov.
Výhodou, ktorú prináša použitie hashu je nemožnosť rekonštruovať pôvodné heslo. V prípade, ak ukladáme heslá v takejto podobe do databázy, odcudzením databázy vzniká útočníkovi problém s rekonštruovaním hesiel. Ak by sme však zostali len pri samotnom hashovom algoritme, má k dispozícii pomôcku – tzv. dictionary(slovníkový) útok, kedy si predgeneruje hashe pre celú alebo špecifickú množinu reťazcov, a jednoduchým porovnaním dokáže veľmi jednoducho napárovať hash a odhaliť heslo.
Nevýhodou použitia hashu je nemožnosť pri heslo rekonštruovať a opätovne ho oznámiť používateľovi. Jediný spôsob, ako mu umožniť znovuprihlásenie je vygenerovať heslo nové, ktoré obdrží podľa možností bezpečným kanálom prípadne bude novovygenerované heslo slúžiť len ako prostriedok pre jeho prvé prihlásenie s následným vynútením si zmeny.
Salt
Existuje však aj spôsob, ako efektívne predísť slovníkovým útokom. Pre každé ukladané heslo vygenerujeme náhodný reťazec - tzv. Salt, ktorý pripojíme ku heslu a až potom vytvoríme hash z celého takto vzniknutého reťazca. Salt uložíme do databázy spolu s výsledným hashom. Útočník by musel pregenerovať tabuľku hashov pre každý cieľový salt, aby mohol použiť slovníkový prístup na jeho spätné získanie. Tým sa jeho snaha výrazne, priam až ketegoricky, časovo skomplikuje.
Naša práca s heslom je však naďalej veľmi jednoduchá. Načítame salt uložený pre daného používateľa, spojíme s heslom, vytvoríme hash a ten porovnáme s hashom uloženým v databáze. Ak sa oba reťazce zhodujú, používateľ zadal správane heslo.
Na tento účel nám postačí pár statických metód. Na vytvorenie Hashu použijeme pre jednoduchosť funkciu FormsAuthentication.HashPasswordForStoringInConfigFile a SHA1 hash algoritmus.(K dispozícii máme ešte MD5 hashový algoritmus. Ten sa však vďaka zisteným vážnym "trhlinám" prestal používať a aj keď pri SHA1 bola tiež trhlina zistená, stále, nie je katerogorická.)
Na vygenerovanie saltu použijeme RNGCryptoServiceProvider - generátor náhodných čísel, ktorý je šítý práve na takýto účel.
using System;
using System.Web.Security;
using System.Security.Authentication;
using System.Security.Cryptography;
namespace ASPNET.Security
{
public class AuthenHelper
{
public static string GetHashedPassword(string password, string salt)
{
String foo;
return SaltPassword(password, salt, out foo);
}
public static string GetHashedPassword(string sPassword, out string sSalt)
{
return SaltPassword(password, null, out salt);
}
private static string SaltPassword(string password, string salt, out string genSalt)
{
genSalt = null;
if (salt == null)
{
RNGCryptoServiceProvider cryptoProvider = new RNGCryptoServiceProvider();
byte[] arrSalt = new byte[16];
cryptoProvider.GetBytes(arrSalt);
salt = Convert.ToBase64String(arrSalt);
genSalt = salt;
}
return FormsAuthentication.HashPasswordForStoringInConfigFile(salt + password, "SHA1");
}
}
}
Modifikovaný kód prihlasovacej stránky z článku by mohol vyzerať takto.
public void LoginCtrl_Authenticate(object sender, AuthenticateEventArgs e)
{
IDACUser user = (IDACUser)DACFactory.Get(DACObject.User);
user.Login = Lgn.Text.ToLower();
//Ziskanie zakladnych udajov o userovi potrebnych pri autentifikacii
if (!user.GetAuthenticationInfo())
{
dvMsg.InnerText = Resources.User.ErrorLoginUser;
dvMsg.Visible = true;
return;
}
string password = Pwd.Text.Trim();
//vytvorenie hashu na porovnanie z prave zadaneho hesla a ulozeneho saltu
string hashedPassword = AuthenHelper.GetHashedPassword(password, user.Salt);
//porovnanie oboch hashov, hashu hesla ulozeneho v db a toho prave vyrobeneho zo zadaneho hesla
if (hashedPassword != user.Password)
{
dvMsg.InnerText = Resources.User.LoginFailure;
dvMsg.Visible = true;
return;
}
TestIdentity identity = new TestIdentity();
identity.Login = user.Login;
identity.ID = user.ID;
identity.DisplayName = user.FirstName +" "+ user.SurName;
identity.Company.ID = user.Company.ID;
identity.Company.Name = user.Company.Name;
CustomPrincipal principal = new CustomPrincipal(identity, new string[] {"Director", "Pensioner"});
HttpContext.Current.User = principal;
string state = principal.SaveStateAsBase64String();
DateTime now= DateTime.Now;
FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1,
identity.Login,
now,
now.AddMinutes(60),
LoginCtrl.RememberMeSet,
state);
string encodedTicket = FormsAuthentication.Encrypt(ticket);
HttpCookie ticketCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encodedTicket);
Response.Cookies.Add(ticketCookie);
string returnUrl = Request.QueryString["ReturnUrl"];
Response.Redirect(!string.IsNullOrEmpty(returnUrl) ? returnUrl : "~/Private.aspx",
true);
}
Kód, kde vytvárame používateľa by mohol vyzerať symbolicky takto:
string salt = null;
user.password = AuthenHelper.GetHashedPassword(TBPwd.Text.Trim(), out sSalt);
user.salt = sSalt;
//...
user.Create();
//...