Jednorázové heslo

Školení, která pořádám

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, diskuse: 7 (nové: 0)

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é…

ikona Jakub Vrána OpenID:

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

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).

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.

ikona Jakub Vrána OpenID:

Díky, doplnil jsem to do článku.

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?

Bohumil:

Vyřešeno. Měl jsem chybu v dekódování do bináru pro generování jednorázového kódu.

Vložit komentář

Používejte diakritiku. Vstup se chápe jako čistý text, ale URL budou převedeny na odkazy a PHP kód uzavřený do <?php ?> bude zvýrazněn. Pokud máte dotaz, který nesouvisí s článkem, zkuste raději diskusi o PHP, zde se odpovědi pravděpodobně nedočkáte.

Jméno: URL:

avatar © 2005-2020 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.