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: 5 (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.

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