Generator haseł – metodologia wyboru z zestawów

Dosyć często używam generatorów haseł. Jakoś nie mam talentu ani sposobu na wymyślanie jakichś finezyjnych, więc polegam na losowo wygenerowanych ciągach zawierających różne zestawy danych. Tradycyjnie takimi zestawami są np: małe litery, duże litery, cyfry, znaki interpunkcyjne oraz inne znaki specjalne. Używając kilku z narzędzi do generowania haseł dostępnych z poziomu www zauważyłem pewną “prawidłowość” – losowanie najprawdopodobniej wykonywane jest na sumie wybranych zbiorów. Przedstawienie bardziej-po-ludzku: załóżmy, że chcemy hasło 9 znakowe, zawierające duże/małe litery oraz cyfry. Przy wielokrotnym generowaniu hasła zauważamy, że cyfry występują nad wyraz rzadko. Często mają miejsce sytuacje, gdy nie jest wylosowana żadna, albo tylko jedna. Czasami trafiają się dwie, zaś większa ilość to istna egzotyka. Statystycznie średnio powinny być jednak 3 cyfry, bo mamy do dyspozycji 3 zbiory i 9 miejsc do wypełnienia. Tak się jednak nie dzieje…

Jedyne co przychodzi mi na myśl, to właśnie wspomniane losowanie z sumy zbiorów, zamiast najpierw zbioru a dopiero później elementu z niego. Losowanie z sumy można by przedstawić przykładowo tak:

$chars = array(
    'upper' => array('Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M'),
    'lower' => array('q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm'),
    'numeric' => array('1', '2', '3', '4', '5', '6', '7', '8', '9', '0'),
    'punctuation' => array('.', ',', ':', ';', '!', '?', '-', '_', '[', ']', '(', ')', '{', '}'),
    'special' => array('@', '#', '$', '%', '^', '*', '+', '=', '/', '\\'),
);
$chars_to_rand = array();

// $checked_options -> tablica z wybranymi zestawami znakow
foreach ($checked_options as $key => $val)
{
    $chars_to_rand = array_merge($chars_to_rand, $chars[$key]);
}

$num_chars = count($chars_to_rand) - 1;
for ($i = 0; $i < $passwordLength; $i++)
{
    $password .= $chars_to_rand[rand(0, $num_chars)];
}

Mamy sobie tablicę trzymającą wszystkie zestawy, mamy jakąś inną która z np. POST/GET wyznacza nam zestawy do wybrania. Następnie tworzymy sumę wybranych zakresów, a na koniec składamy ciąg znaków biorąc losowy element z tej sumy. Oczywiście w tym przypadku zakładamy możliwość wystąpienia powtórzeń. W opisywanej wcześniej sytuacji generator nie będzie jednak równomiernie wybierał z różnych zakresów. Porównując litery a liczby: pierwszych jest łącznie 52, drugich tylko 10. W efekcie cyfra zamiast wystąpić w około 1/3 przypadków, zrobi to w zaledwie niecałej 1/5 losowań. Wynika to właśnie z tego, że losujemy z sumy a nie z samych zakresów. Przeróbmy więc ten fragment, aby uzyskać zakładaną ilość wystąpień:

$chars = array(
    'upper' => array('Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X', 'C', 'V', 'B', 'N', 'M'),
    'lower' => array('q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g', 'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm'),
    'numeric' => array('1', '2', '3', '4', '5', '6', '7', '8', '9', '0'),
    'punctuation' => array('.', ',', ':', ';', '!', '?', '-', '_', '[', ']', '(', ')', '{', '}'),
    'special' => array('@', '#', '$', '%', '^', '*', '+', '=', '/', '\\'),
);
$chars_to_rand = array();

// $checked_options -> tablica z wybranymi zestawami znakow
foreach ($checked_options as $key => $val)
{
    $chars_to_rand[] = array(
        'chars' => $chars[$key],
        'num' => count($chars[$key]) - 1,
    );
}

$num_arrays = count($chars_to_rand) - 1;
for ($i = 0; $i < $passwordLength; $i++)
{
    $rand_array = rand(0, $num_arrays); // tu losujemy zestaw
    $rand_chars = $chars_to_rand[$rand_array]['num'];
    $password .= $chars_to_rand[$rand_array][rand(0, $rand_chars)]; // tu losujemy znak
}

Początek nie uległ zmianie. Gdy jednak sprawdzamy jakie opcje wybrano, nie sklejamy już sumy zbiorów, ale kopiujemy je do zachowując strukturę tablicy, a dodatkowo obliczamy ile elementów jest w każdym ze zbiorów. Przed losowaniem liczymy, ile zbiorów posiadamy. Podczas samego generowania najpierw wybieramy losowy zestaw znaków, a następnie sam znak. Oczywiście przedstawiona powyżej forma nie jest tak elegancka jak mogłaby być, acz chyba pokazuje, o co chodzi – tym razem głównym wyznacznikiem co zostanie wylosowane nie jest znak jako taki, lecz jego zestaw: cyfry są w stosunku 1:2 do liter, a więc nasze pierwotne zamierzenie jest zrealizowane.

Oczywiście mogę się mylić, być może generatory z których miałem okazję korzystać (pierwsze pozycje na wynikach wyszukiwania google), działają w zupełnie inny sposób, niemniej na tyle często ich używałem, że ta niska częstotliwość występowania czy to cyfr, czy znaków specjalnych, skusiła mnie to takich rozważań. Jeżeli macie inne pomysły, podzielcie się nimi.