Jednorázové heslo

Do svých instalací Admineru jsem si přidělal ověřování jednorázového hesla (OTP – One Time Password, které se dá použít pro Two-step authentication), zadaného např. pomocí Google Authenticatoru nebo podobné aplikace. Překvapilo mě, jak to bylo jednoduché a doporučoval bych tuto možnost dodělat do všech aplikací s přihlašováním heslem.

Pro vygenerování tajemství sdíleného mezi Google Authenticatorem a vaší aplikací stačí zavolat random_bytes(10). Pokud chceme možnost použití OTP dát všem uživatelům systému (a ne třeba jen jednomu administrátorovi), tak je vhodné každému vygenerovat vlastní tajemství a uložit ho do databáze.

Do aplikace se tento kód přenáší zakódovaný v Base32, k čemuž lze použít jednoduchou funkci:

<?php
/** Zakódování řetězce do Base32
* @param string řetězec délky dělitelné pěti
* @return string
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function base32_encode($data) {
    static $codes = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    $bits = "";
    foreach (str_split($data) as $c) {
        $bits .= sprintf("%08b", ord($c));
    }
    $return = "";
    foreach (str_split($bits, 5) as $c) {
        $return .= $codes[bindec($c)];
    }
    return $return;
}
?>

Google Authenticator dovoluje informace i naskenovat pomocí QR kódu, k čemuž můžeme využít Google Charts API:

<?php
/** Vygenerování URL s obrázkem QR kódu pro OTP
* @param string název služby
* @param string uživatelské jméno
* @param string binární podoba tajemství
* @return string URL
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function getOtpQrUrl($issuer, $user, $secret) {
    $otpAuth = "otpauth://totp/" . rawurlencode($issuer) . ":$user?secret=" . base32_encode($secret) . "&issuer=" . rawurlencode($issuer);
    return "https://chart.googleapis.com/chart?chs=200x200&chld=M|0&cht=qr&chl=" . urlencode($otpAuth);
}
?>

Pokud informaci o tajemství nechcete předávat třetí straně, můžete QR kód vygenerovat lokálně, např. pomocí knihovny QR Code.

Při ověřování kódu zadaného uživatelem vygenerujeme ten stejný kód:

<?php
/** Vygenerování jednorázového hesla
* @param string binární podoba tajemství
* @param string časový slot, typicky floor(time() / 30)
* @return int
* @copyright Jakub Vrána, https://php.vrana.cz/
*/
function getOtp($secret, $timeSlot) {
    $data = str_pad(pack('N', $timeSlot), 8, "\0", STR_PAD_LEFT);
    $hash = hash_hmac('sha1', $data, $secret, true);
    $offset = ord(substr($hash, -1)) & 0xF;
    $unpacked = unpack('N', substr($hash, $offset, 4));
    return ($unpacked[1] & 0x7FFFFFFF) % 1e6;
}
?>

Takto vygenerovaný kód stačí porovnat s tím, co zadal uživatel, protože aplikace ho generuje stejně. Funkce pracuje s šesticifernými kódy, které jsou výchozí. Pokud ověření selže, tak bych doporučoval porovnat i kód pro předchozí (pro případ, že uživatel kód nestihl opsat včas) a následující (pokud se rozchází čas) $timeSlot.

Jakub Vrána, Seznámení s oblastí, 23.2.2018, on-line

Diskuse

Franta:

Ad „Google Authenticator dovoluje informace i naskenovat pomocí QR kódu, k čemuž můžeme využít Google Charts API“

Předávat soukromé klíče / tajemství Googlu (nebo libovolné třetí straně) mi nepřijde zrovna dvakrát moudré…
3.3.2018 20:16:03

ikona Jakub Vrána:

Souhlasím. Máš nějaký tip na jednoduchou knihovnu na generování QR kódů lokálně?
5.3.2018 08:44:43

Franta:

Podle jazyka/platformy. V Javě se používá ZXing: https://github.com/zxing/zxing (na té stránce jsou i odkazy na jiné implementace včetně PHP a JavaScriptu).
6.3.2018 23:20:00

Karel Borkovec:

Není ten ZXing pouze pro čtení QR kódů? Nevidím tam, že by je uměl i generovat. Pro PHP je třeba https://github.com/endroid/qr-code, ale osobní zkušenost s tím nemám.
7.3.2018 10:03:57

ikona Jakub Vrána:

Díky, doplnil jsem to do článku.
11.3.2018 15:42:09

Bohumil:

Od serveru obdržím do aplikace např. tento kód:
VUTHONVANAYCFSX5
Po zadání (záměrně ručně) je generován funkční ověřovací kód. Pokud si jej generuji sám, dle výše uvedeného popisu, obdržím zcela jinou číselnou kombinaci, než generuje aplikace.

Pro chráněný vstup  využívám ověření uživatele proti LDAP a potřebuji pro dvoufázové ověření tedy shodu v ověřovacím kódu, který obdrží uživatel z aplikace. Zatím nejsem s to vygenerovat totožný a netuším proč. Můžete mi někdo poradit?
13.3.2020 15:39:53

Bohumil:

Vyřešeno. Měl jsem chybu v dekódování do bináru pro generování jednorázového kódu.
14.3.2020 16:34:16
avatar © 2005-2024 Jakub Vrána. Publikované texty můžete přetiskovat pouze se svolením autora. Ukázky kódu smíte používat s uvedením autora a URL tohoto webu bez dalších omezení Creative Commons. Můžeme si tykat. Skripty předpokládají nastavení: magic_quotes_gpc=Off, magic_quotes_runtime=Off, error_reporting=E_ALL & ~E_NOTICE a očekávají předchozí zavolání mysql_set_charset. Skripty by měly být funkční v PHP >= 4.3 a PHP >= 5.0.