Na Intervalu jsme se už prošli základy kryptografie v .NET. Tentokrát se zaměříme na palčivou otázku z praxe – hesla. Přestože neexistuje stoprocentně bezpečný způsob, jak hesla uchovávat, měli bychom se snažit to dělat co možná nejbezpečněji. Všechny ukázky budou napsány v ASP.NET, teoretické postupy jsou však více či méně společné pro všechna prostředí.

Hodně webů vyžaduje, aby uživatel před vstupem zadal svoje osobní heslo. Ať už to je firemní web, kde si můžete třeba jen objednat oběd na další den, nebo emailová schránka, přístup neoprávněné osoby je naprosto nežádoucí. Bylo by proto vhodné chránit tato hesla co možná nejlepším způsobem před zraky ostatních. Jsou dva typy „problémů“.

Jedním z nich je sám uživatel, který si zvolí heslo naprosto nevyhovující – například anakonda nebo traktor nebo dokonce jméno nějakého příbuzného. Pokud na takovéto heslo použije útočník takzvaný slovníkový útok, má během několika minut vyhráno. V rámci zapamatovatelnosti hesla se proto doporučuje nějaká kombinace alespoň dvou slov, pokud možno s použitím nějakých „nestandardních“ znaků (pomlčka, podtržítko a podobně) nebo čísel – například traktor_zetor či 10alik01. Ovšem pokud docílíte i tohoto, zdánlivě nemožného cíle (do uživatelů můžete hučet jak do dubu), nemáte vyhráno.

Za prvé, i takovéto heslo se dá odhadnout, i když už to není tak jednoduché. Druhým problémem jsou však sami vývojáři. Pokud i vhodně zvolené heslo je uloženo v nějakém datovém úložišti (ať v databázi nebo XML souboru) ve své textové, čitelné podobě, má útočník hodně možností, jak si heslo vytáhnout. Pro SQL databáze například SQL injection (do nedávna nejběžnější způsob útoku) nebo pro soubory XML stačí nezabezpečený adresář a není problém ani pro amatéra nechat si soubor zobrazit v prohlížeči. A hned má útočník všechna jména a hesla jako na dlani.

Hesla hešujte!

Pro vás, jako pro vývojáře, by se proto mělo stát samozřejmostí hesla uchovávat v nečitelné podobě. Jako vhodný způsob se nabízí hesla hešovat. Hešová podoba slova se dá totiž jen těžko zpětně dešifrovat do původní podoby. I kdyby znal útočník použitý algoritmus, dalo by mu hodně práce rekonstruovat původní text. Takový útočník si pak raději zvolí jiný cíl útoku. A to nám stačí.

Pokud způsob ukládání hesel a následného přihlašování uživatele znázorním schematicky, mělo by to vypadat zhruba následovně:

Schéma ukládání hesel a přihlašování uživatele

Znovu však upozorňuji, že znázorněný postup není odolný proti slovníkovým útokům!

Podíváme-li se na problém z pohledu kódu, mohlo by to v ASP.NET vypadat třeba takto:

<%@ Page Language=“C#“ %>
<%@ import Namespace=“System.Security.Cryptography“ %>
<%@ import Namespace=“System.Xml“ %>
<%@ import Namespace=“System.Data“ %>
<script runat=“server“>
  string GetHash(string input)
  {
      byte[] bInputData = ASCIIEncoding.ASCII.GetBytes(input);
  
      MD5 objMD5 = new MD5CryptoServiceProvider();
      byte[] bHashResult = objMD5.ComputeHash(bInputData);
  
      return ASCIIEncoding.ASCII.GetString(bHashResult);
  }
  
  void NewUser(object sender, EventArgs e)
  {
    bool userExists = false;
    DataSet ds = new DataSet();
  
    ds.ReadXml(Server.MapPath(„users.xml“));
  
    DataTable dt = ds.Tables[0];
  
    // test na kolizi dat
    DataRow[] foundRows = dt.Select(„nick = ‚“+tbNick.Text+“‚“);
    if(foundRows.Length > 0) userExists = true;
  
    if(!userExists)
    {
      DataRow dr = dt.NewRow();
  
      dr[„nick“] = tbNick.Text;
      dr[„heslo“] = GetHash(tbPass.Text);
      lblOut.Text = GetHash(tbPass.Text);
  
      dt.Rows.Add(dr);
      ds.AcceptChanges();
      SaveDS(ds);
    }
  }
  
  private void SaveDS(DataSet ds)
  {
    if (ds == null) { return; }
  
    System.IO.FileStream fs;
    System.Xml.XmlTextWriter writer = null;
  
    string filename = Server.MapPath(„users.xml“);
  
    fs = new System.IO.FileStream(filename, System.IO.FileMode.Create);
  
    writer = new System.Xml.XmlTextWriter(fs, System.Text.Encoding.Unicode);
    writer.Formatting = Formatting.Indented;
    writer.Indentation = 5;
  
    ds.WriteXml(writer);
  
    if (writer != null)
      writer.Close();
  }
  
  void TestUser(object sender, EventArgs e)
  {
    XmlTextReader xtr = null;
    bool auth = false;
  
    try
    {
      xtr = new XmlTextReader(Server.MapPath(„users.xml“));
  
      while (xtr.Read())
      {
        if (xtr.Name == „user“)
        {
          if (xtr.GetAttribute(„heslo“) == GetHash(tbPass.Text))
            auth = true;
        }
      }
    }
    finally
    {
      if (xtr != null)
        xtr.Close();
    }
  
    lblOut.Text = auth ? „ok“ : „špatný nick nebo heslo!“;
  }
</script>
<html>
<head>
</head>
<body>
  <form runat=“server“>
    Nick:
    <asp:TextBox id=“tbNick“ runat=“server“></asp:TextBox>
    <br />
    Heslo:
    <asp:TextBox id=“tbPass“ runat=“server“></asp:TextBox>
    <br />
    <asp:Button id=“btnOk“ onclick=“TestUser“ runat=“server“ text=“Odeslat!“></asp:Button>
    <asp:Button id=“btnNew“ onclick=“NewUser“ runat=“server“ text=“Nový“></asp:Button>
    <br />
    <asp:Label id=“lblOut“ runat=“server“></asp:Label>
  </form>
</body>
</html>

Databáze uživatelů:

<userlist>
   <user nick=“rj“ heslo=“???&#x1;S?&#x1C;?&#xD;??%p?&#x18;?“ />
   <user nick=“gates“ heslo=“?]??&#x1E;???JM???a??“ />
</userlist>

V příkladu jsem jako úložiště dat použil soubor XML. Teoretický postup při ukládání do jiných datových skladů je však analogický. Nyní bychom si mohli uvedený příklad projít krůček po krůčku.

string GetHash(string input)
{
byte[] bInputData = ASCIIEncoding.ASCII.GetBytes(input);
MD5 objMD5 = new MD5CryptoServiceProvider();
byte[] bHashResult = objMD5.ComputeHash(bInputData);
return ASCIIEncoding.ASCII.GetString(bHashResult);
}

Toto je jednoduchá metoda, která vstupní text hešuje algoritmem MD5 a vrací řetězec, který již můžeme celkem směle uložit do databáze. Uvedenou metodu voláme jak při ukládání, tak i při testování správnosti hesla.

void NewUser(object sender, EventArgs e)
{
bool userExists = false;
DataSet ds = new DataSet();
ds.ReadXml(Server.MapPath(„users.xml“));
DataTable dt = ds.Tables[0];
// test na kolizi dat
DataRow[] foundRows = dt.Select(„nick = ‚“+tbNick.Text+“‚“);
if(foundRows.Length > 0) userExists = true;
if(!userExists)
{
DataRow dr = dt.NewRow();
dr[„nick“] = tbNick.Text;
dr[„heslo“] = GetHash(tbPass.Text);
// ukladame hashovanou podobu
dt.Rows.Add(dr);
ds.AcceptChanges();
SaveDS(ds);
}
}

Toto je metoda, která vytváří nového uživatele. Nás nyní zajímá především řádek dr["heslo"] = GetHash(tbPass.Text);. Tady voláme již zmíněnou metodu GetHash() a její výstup uložíme. Když se podíváte na soubor s uživateli, vidíte, jak zhruba vypadá výstup této metody. Testování uživatele při přihlašování může vypadat následovně:

void TestUser(object sender, EventArgs e)
{
XmlTextReader xtr = null;
bool auth = false;
try
{
xtr = new XmlTextReader(Server.MapPath(„users.xml“));
while (xtr.Read())
{
if (xtr.Name == „user“)
{
if (xtr.GetAttribute(„heslo“) == GetHash(tbPass.Text))
auth = true;
}
}
}
finally
{
if (xtr != null)
xtr.Close();
}
lblOut.Text = auth ? „ok“ : „špatný nick nebo heslo!“;
}

Tato metoda prochází úložiště dat a testuje uloženou hešovou podobu hesla s hešem hesla zadaného do textboxu. Pokud se heše shodují, je vše vpořádku a uživatel může být přihlášen. Zde zároveň vidíte, že není třeba používat některý ze symetrických či nesymetrických šifrovacích algoritmů, protože není třeba uloženou hodnotu dešifrovat. Možnost dešifrovat reprezentaci hesla je tady přímo nežádoucí! Dali bychom totiž útočníkovi další možnost, jak se k heslu dostat.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Žádný příspěvek v diskuzi

Odpovědět