Argon2
Napisał: Patryk Krawaczyński
01/09/2021 w Bezpieczeństwo Brak komentarzy. (artykuł nr 794, ilość słów: 1135)
A
rgon2 jest algorytmem, który wygrał w 2015 roku Konkurs Haszowania Haseł (ang. Password Hashing Competition). Jest wskazywany jako następca bcrypt i scrypt oraz zalecany przez OWASP (ang. The Open Web Application Security Project) do zabezpieczania haseł. Zaletą tego algorytmu są jego różne warianty (aktualnie posiada on trzy: Argon2i, Argon2d i Argon2id), które zawierają mechanizmy utrudniające ataki typu brute force oraz side channel. Ma prostą konstrukcję mającą na celu uzyskanie najwyższego współczynnika wypełnienia pamięci i efektywnego wykorzystania wielu jednostek obliczeniowych.
Jeśli chodzi o warianty to Argon2d jest szybszy i wykorzystuje dostęp do pamięci zależny od danych, co czyni go wysoce odpornym na ataki łamania przy pomocy GPU i nadaje się do aplikacji, które nie są zagrożone atakami typu side-channel. Sprawia to, że jest mniej odpowiedni do haszowania sekretów, a bardziej odpowiedni dla kryptowalut. Argon2i w kontrze do 2d używa niezależnego od danych dostępu do pamięci, co jest preferowane w przypadku haszowania haseł i funkcji wyprowadzania kluczy. Jest wolniejszy, ponieważ wykonuje więcej przejść przez pamięć w celu ochrony przed atakami kompromisu czasu – pamięci (ang. tradeoff attacks). Argon2id – jest hybrydą Argonu 2d i 2i wykorzystującą kombinację zależnego i niezależnego od danych dostępu do pamięci, co daje większą odporność na ataki kanału bocznego oraz łamania kartami graficznymi.
Dlaczego “po prostu użyj bcrypt” nie jest już najlepszą odpowiedzią? Obecnymi końmi roboczymi haszowania haseł są bezsprzecznie bcrypt i PBKDF2. I chociaż nadal można ich używać to społeczność zajmująca się łamaniem haseł wykorzystuje od dłuższego czasu coraz lepsze procesory graficzne i układy ASIC do łamania haseł w sposób wysoce równoległy. Skutecznym środkiem przeciwko ekstremalnej równoległości okazało się uczynienie obliczeń haseł trudnymi ze względu na wymogi pamięci. Najbardziej znaną implementacją tego podejścia jest do tej pory scrypt. Jednak w specyfikacji algorytmu Argon2 możemy przeczytać, że ataki kompromisu czasu – pamięci pozwalają na efektywne dystrybuowanie obliczeń w celu redukcji zarówno czasu, jak i pamięci przy tym samym koszcie energetycznym. Dlatego zaistniała potrzeba stworzenia nowego algorytmu. Uwzględniającego aktualne ataki, który zostanie zweryfikowany przez niezależną komisję zamiast pojedynczych implementatorów.
Sam algorytm jest dostępny od wersji PHP 7.2 (zaproponowany w RFC) dlatego możemy z niego skorzystać już w Ubuntu 18.04 LTS. Przykład użycia algorytmu Argon2 jest bardzo prosty. Możemy go wykorzystać poprzez parametr $algo
w funkcji password_hash()
. Na początek instalujemy niezbędne pakiety:
sudo apt install php7.2-cli argon2 libargon2-0 libargon2-0-dev
Następnie preparujemy prosty plik .php, w którym drugi parametr określa algorytm używany podczas haszowania; algorytm Argon2i jest reprezentowany przez stałą PASSWORD_ARGON2I
:
root@darkstar:~# cat argon2.php <?php $password = 'test'; $hash = password_hash($password, PASSWORD_ARGON2I); var_dump($hash); ?> root@darkstar:~# php argon2.php string(96) "$argon2i$v=19$m=65536,t=4, p=1$NlhobGpBMHAyNmJUZmhjaA$UaEF/jVMPKQ8aISBERL+M/61OWxO+B/wWyFxEUXsKe0"
Zwrócony ciąg znaków składa się z kilku części oddzielonych od siebie znakiem dolara ($
):
argon2i v=19 m=65536,t=4,p=1 NlhobGpBMHAyNmJUZmhjaA UaEF/jVMPKQ8aISBERL+M/61OWxO+B/wWyFxEUXsKe0
Pierwsza część to nazwa algorytmu (argon2i), druga to jego wersja (19), a trzecia część to lista parametrów algorytmu związanych z kosztem pamięci (w KB), kosztem czasu i używanymi wątkami (paralelizm). Czwarty parametr to losowa wartości soli, zakodowana w Base64. Wartość ta jest generowana przez funkcję password_hash()
przy użyciu losowej wartości dla każdego wykonania. To dlatego mamy różne wyniki hashowania dla tego samego ciągu wejściowego (tutaj: “test”). Domyślny rozmiar soli to 16 bajtów. Piąty i ostatni parametr wyjściowego łańcucha zawiera wartość haszu (również zakodowany w Base64). Jego rozmiar to 32 bajty.
PHP udostępnia funkcję o nazwie password_get_info($hash)
, aby uzyskać informacje o haszu wygenerowanym przez password_hash()
. Na przykład, jeśli użyjemy password_get_info()
na poprzedniej wartości otrzymamy:
root@darkstar:~# cat argon2.php <?php $password = 'test'; $hash = password_hash($password, PASSWORD_ARGON2I); var_dump($hash); $reverse = password_get_info($hash); var_dump($reverse); ?> root@darkstar:~# php argon2.php string(96) "$argon2i$v=19$m=65536,t=4, p=1$ZDhza0ZkeVB0Nm9xdmI3bQ$h6xi+0Scjc2ThClZM98bn/Uhg+BuNVOwN7z844cjb44" array(3) { ["algo"]=> int(2) ["algoName"]=> string(7) "argon2i" ["options"]=> array(3) { ["memory_cost"]=> int(65536) ["time_cost"]=> int(4) ["threads"]=> int(1) } }
Wartości te można zmienić za pomocą parametru $options
funkcji password_hash()
:
root@darkstar:~# cat argon2.php <?php $password = 'test'; $options = [ 'memory_cost' => 1024, 'time_cost' => 4, 'threads' => 4, ]; $hash = password_hash($password, PASSWORD_ARGON2I, $options); var_dump($hash); $reverse = password_get_info($hash); var_dump($reverse); ?> root@darkstar:~# php argon2.php string(95) "$argon2i$v=19$m=1024,t=4, p=4$RTA0eFpEY25TWG1jbS9qcA$IFenvsdbwfmQp2n8c4ePB0DfQAN5/AStHfLiH0yM08g" array(3) { ["algo"]=> int(2) ["algoName"]=> string(7) "argon2i" ["options"]=> array(3) { ["memory_cost"]=> int(1024) ["time_cost"]=> int(4) ["threads"]=> int(4) } }
Specyfikacja algorytmu Argon2 sugeruje użycie potęgi 2’ki dla kosztu pamięci. Jeśli chodzi o pamięć używaną przez algorytm (memory_cost
) to, aby uczynić łamanie haszy kosztowym dla atakującego powinniśmy ustawić wartość tą na najwyższym możliwym poziomie. Parametr time_cost
to koszt czasu, który określa czas wykonania algorytmu oraz liczbę iteracji przejścia przez pamięć. Czas wykonania jest liniowo skorelowany z tym parametrem. Pozwala on na zwiększenie kosztu obliczeniowego potrzebnego do obliczenia jednego hasza. Ostatnia konfiguracja, czyli threads
to liczba wątków do wykorzystania. Ta również powinna być tak duża, jak to tylko możliwe, aby zmniejszyć zagrożenie związane z równoległym łamaniem haszy. Aby nie wyczerpać przypadkowo zasobów systemowych powinniśmy dostosować omówione współczynniki kosztu obliczeń do możliwości systemu (CPU + RAM), na którym będzie pracował algorytm. Można powiedzieć, że nastawy Argon2 nie mają “złych” wartości, jednakże zużywanie większej ilości zasobów jest uważane za lepsze niż przeznaczenie ich mniejszej ilości. Poniższa lista przedstawia wydajność haszowania na różnych konfiguracjach przy użyciu wartości: memory_cost
= 1 MiB, time_cost
= 2, threads
= 2:
- Serwer wirtualny w chmurze: 512 MB RAM, 1 Core CPU: 3-5 ms
- Serwer wirtualny w chmurze: 2 GB RAM, 2 Core CPU: 1-3 ms
- Komputer jednopłytowy 512 MB Raspberry Pi Zero: 75-85ms
Aby osiągnąć pożądany dla nas czas wykonania, możemy manipulować dwiema zmiennymi. Zaleca się, aby zacząć od największej możliwej ilości pamięci i jednej iteracji. Następnie możemy zmniejszyć pamięć, aż jedna operacja haszowania zajmie mniej niż pożądany czas. Następnie zwiększajmy liczbę iteracji, aby zbliżyć się do pożądanego czasu wykonania tak blisko, jak to tylko możliwe. Jeśli chodzi o błędy to należy obserwować komunikaty E_WARNING
, które będą generowane dla wartości, które nie mogą być użyte jako opcje dla algorytmu PASSWORD_ARGON2I
.
Implementacja w języku PHP jest tylko przykładem. Do wyboru mamy także inne języki takie jak C, Python, Java, czy NodeJS. W dodatku inne implementacje pozwalają na modyfikację długości soli oraz haszu właściwego.
Więcej informacji: Time/memory/data tradeoff attack, Side-channel attack, Argon2 Discussion Issues, Choose Argon2 Parameters for Secure Password Hashing and Login, Protecting passwords with Argon2 in PHP 7.2, Password Hashing: Scrypt, Bcrypt and ARGON2