Výpočet síly hesla

Nedávno jsem potřeboval do jedné aplikace zavést nějaký výpočet síly hesla a předejít tak zadávání slabých hesel. Po chvíli googlení jsem našel jeden zápisek. Po implementaci popsaného výpočtu jsem provedl ještě několik úprav.

Napřed jsem změnil požadovanou délku hesla - jsem lehce paranoidní a tak tedy požaduji, aby to heslo mělo alespoň 6 znaků. Dále jsem doplnil kontrolu na sekvence - heslo 123456 nebo abcdef podle původních kontrol dostalo docela dost, přestože mít takovéhle heslo není vůbec dobrý nápad. K tomu jsem ještě sehnal aspoň nějaký slovník slov (bude přiložen) a při kontrole hledám shody ve slovníku se zadaným heslem. Nakonec jsem ještě pozměnil bodování.

Při testech původní implementace jsem dostával pocit, že velkou část skóre dostávají hesla, která bych rozhodně povolit nechtěl a naproti tomu aspoň trochu dobrá hesla jsou ohodnocovány docela vysoko. Výsledkem bylo, že člověk viděl jak moc je to heslo k ničemu, naproti tomu dobré heslo od ještě lepšího nerozlišil. Původní přidávání 10 nebo 15 bodů jsem snížil na přidávání pouze 5ti bodů a posunul jsem hodnotící hranice. Výsledek vypadal docela pěkně - při testu na živých objektech :-) se skóre pohybovalo okolo 50 bodů (ovšem u technicky zdatných jedinců, abych se přiznal). Naproti tomu plných 100 bodů dosáhne až opravdu brutální heslo (musí mít minimálně 18 znaků a to ještě při tom musí splnit všechny bonusové požadavky a naopak se nedopustit žádného prohřešku). Hranice, které považuji za vhodné, jsou takovéhle:

  • Pod 20 bodů - moc slabé, nebrat
  • Pod 30 bodů - rozumné a použitelné heslo
  • Pod 45 bodů - dobré heslo
  • Pod 60 bodů - silné heslo
  • Pod 80 bodů - velice silná hesla, avšak už dosti nezapamtovatelná
  • 80 - 100 bodů - šílená hesla :-)

Současný výpočet skóre (hodnota od 0 do 100, kde 100 je nejsilnější heslo) vypadá takto:

  • Pokud je heslo kratší než 6 znaků nebo je shodné s uživatelským jménem, skóre je 0 a končí se
  • Jako základní skóre se vezme délka hesla a vynásobí se čtyřma
  • Prozkoumá se počet opakujících se znaků a jejich celkový počet (všechny znaky, u kterých je opakování) se odečte ze skóre. Tedy pokud je heslo třeba aabbcd, odečte se 4 (protože 2x a + 2x b). Navíc pokud počet opakovaných znaků (v minulém příkladu jsou dohromady 2 - a má první znak a druhé a je první opakující se a stejně tak b, dohromady tedy 2) je větší nebo roven než polovina délky hesla a zároveň samotný počet znaků, které se opakují (tedy třeba u hesla aabbcd jsou 2 - a se opakuje a b se opakuje, u hesla aaabcde je to 1 - opakuje se pouze a) je 2 nebo menší, strhne se navíc ještě 15 bodů
  • Pokud heslo obsahuje alespoň tři číslice, přičte se 5 bodů
  • Pokud heslo obsahuje alespoň 2 speciální znaky, přičte se 5 bodů
  • Pokud heslo obsahuje malé i velké písmena, přičte se 5 bodů
  • Pokud heslo obsahuje číslice a písmena, přičte se 5 bodů
  • Pokud heslo obsahuje číslice a speciální znaky, přičte se 5 bodů
  • Pokud heslo obsahuje písmena a speciální znaky, přičte se 5 bodů
  • Pokud heslo obsahuje jen písmena, odečte se 10 bodů
  • Pokud heslo obsahuje jen číslice, odečte se 10 bodů
  • Nyní se vyšetří sekvence, tedy písmena a čísla, která jsou po sobě (abcdef, 123456, platí jak pro malá tak velká písmena). Po vyšetření se počítá s počtem nalezených sekvencí a délkou každé z nich. Heslo 123456 obsahuje jednu sekvenci o délce 6, heslo 12qabcd obsahuje 2 sekvence, jednu o délce 2, druhou o délce 6. Ze skóre se odečte (počet sekvencí +1)součet délek sekvencí, u prvního hesla je to tedy 26, u druhého 35
  • Stejně tak se vyšetří a ohodnotí obrácené sekvence, tedy 6543210, dcba a podobně
  • Prohledá se slovník - slova ze slovníku se hledají v hesle a za každý nalezený výskyt se odečte 20 bodů. Pokud máme heslo motorka a slovník obsahuje slova motorka i motor, odečte se 40 bodů, protože obě tyto slova se v hesle najdou
  • Jelikož chceme výsledek od 0 do 100 a během tohoto počítání jsme se mohli dostat přes 100 nebo i pod 0, ořežeme skóre na maximální/minimální hodnotu, v případě že bylo překročeno

Implementace v pythonu vypadá takto:

  1. import re
  2.  
  3. def convertOrd(char):
  4.     if (char >= ord("a") and char <= ord("z")):
  5.         return char - 32
  6.     else:
  7.         return char
  8.  
  9. def getSequences(password):
  10.     sequences = []
  11.     pos = 0
  12.     while (pos < len(password)):
  13.         charnum = 0
  14.         try:
  15.             charnum = ord(password[pos])
  16.         except:
  17.             pos += 1
  18.             continue
  19.         charnum = convertOrd(charnum)
  20.         if ((charnum >= ord("A") and charnum <= ord("Z")) or (charnum >= ord("0") and charnum <= ord("9"))):
  21.             x = 1
  22.             seqlen = 0
  23.             while (pos + x < len(password)):
  24.                 charnum2 = 0
  25.                 try:
  26.                     charnum2 = ord(password[pos+x])
  27.                 except:
  28.                     if (seqlen > 0):
  29.                         sequences.append(seqlen)
  30.                     x += 1
  31.                     break
  32.                 charnum2 = convertOrd(charnum2)
  33.                 if (charnum + 1 == charnum2 and (charnum2 <= ord("Z") or charnum2 <= ord("9"))):
  34.                     seqlen += 1
  35.                     x += 1
  36.                     charnum = charnum2
  37.                 else:    
  38.                     break
  39.             if (seqlen > 0):
  40.                 sequences.append(seqlen)
  41.             pos += x
  42.         else:
  43.             pos += 1
  44.     return sequences
  45.  
  46. def dictionaryCount(password):
  47.     # Vrátí počet slov ve slovníku nalezených v hesle
  48.     f = open("slovnik", "r")
  49.     count = 0
  50.     for line in f:
  51.         line = line.rstrip("\n")
  52.         #print line
  53.         if (password.find(line) != -1):
  54.             count += 1
  55.     f.close()
  56.     return count
  57.  
  58. def passwordStrength(password, username):
  59.     # Vypočítá skóre od 0 do 100 pro dané heslo
  60.     score = 0
  61.     if (not (password == username or len(password) < 6)):
  62.         score = len(password) * 4
  63.         rep = 0
  64.         repchars = 0
  65.         pos = 0
  66.         tested = []
  67.         for i in password:
  68.             if (not i in tested):
  69.                 tested.append(i)
  70.                 if (re.match("[\d\w]", i)):
  71.                     count = len(re.findall(i, password[pos+1:]))
  72.                     if (count > 0):
  73.                         score -= count
  74.                         rep += count
  75.                         repchars += 1
  76.             pos += 1
  77.         score -= repchars
  78.         if (repchars <= 2 and rep >= len(password)/2):
  79.             score -= 15
  80.         numcount = len(re.findall("\d", password))
  81.         if (numcount >= 3):
  82.             score += 5
  83.         spcharscount = len(re.findall("[-.\[\]!@#&\,%\^&*?_~€\(\)<>}{°]", password))
  84.         if (spcharscount >= 2):
  85.             score += 5
  86.         lowcasecount = len(re.findall("[a-z]", password))
  87.         upcasecount = len(re.findall("[A-Z]", password))
  88.         if (lowcasecount > 0 and upcasecount > 0):
  89.             score += 5
  90.         if (numcount > 0 and (lowcasecount > 0 or upcasecount > 0)):
  91.             score += 5
  92.         if (numcount > 0 and spcharscount > 0):
  93.             score += 5
  94.         if (spcharscount > 0 and (lowcasecount > 0 or upcasecount > 0)):
  95.             score += 5
  96.         if (lowcasecount + upcasecount == len(password)):
  97.             score -= 10
  98.         if (numcount == len(password)):
  99.             score -= 10
  100.         # Sekvence
  101.         seq = getSequences(password)
  102.         for i in seq:
  103.             score -= (len(seq) +1) ** (i+1)
  104.         seq = getSequences(password[::-1])
  105.         for i in seq:
  106.             score -= (len(seq) +1) ** (i+1)
  107.         # Slovník
  108.         score -= 20 * dictionaryCount(password)
  109.         if (score > 100):
  110.             score = 100
  111.         elif (score < 0):
  112.             score = 0
  113.     return score

Daný zdrojový kód je ke stažení včetně slovníku zde. Také si můžete hned vyzkoušet online verzi.