Files
librenms-librenms/LibreNMS/Authentication/TwoFactor.php
Lucas Dousse 3e0c26ff50 webui: Make two-factor auth logo use response (#8652)
If you customise your logo and use png the logo on the two auth is not responsive. I purpose to add class
2018-05-08 22:51:21 +01:00

319 lines
9.6 KiB
PHP

<?php
/**
* TwoFactor.php
*
* Two-Factor Authentication Library
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @license GPL
* @package LibreNMS
* @link http://librenms.org
* @subpackage Authentication
* @author f0o <f0o@devilcode.org>
* @copyright 2014 f0o, LibreNMS
* @copyright 2017 Tony Murray
*/
namespace LibreNMS\Authentication;
use LibreNMS\Config;
use LibreNMS\Exceptions\AuthenticationException;
class TwoFactor
{
/**
* Key Interval in seconds.
* Set to 30s due to Google-Authenticator limitation.
* Sadly Google-Auth is the most used Non-Physical OTP app.
*/
const KEY_INTERVAL = 30;
/**
* Size of the OTP.
* Set to 6 for the same reasons as above.
*/
const OTP_SIZE = 6;
/**
* Window to honour whilest verifying OTP.
*/
const OTP_WINDOW = 4;
/**
* Base32 Decoding dictionary
*/
private static $base32 = array(
"A" => 0,
"B" => 1,
"C" => 2,
"D" => 3,
"E" => 4,
"F" => 5,
"G" => 6,
"H" => 7,
"I" => 8,
"J" => 9,
"K" => 10,
"L" => 11,
"M" => 12,
"N" => 13,
"O" => 14,
"P" => 15,
"Q" => 16,
"R" => 17,
"S" => 18,
"T" => 19,
"U" => 20,
"V" => 21,
"W" => 22,
"X" => 23,
"Y" => 24,
"Z" => 25,
"2" => 26,
"3" => 27,
"4" => 28,
"5" => 29,
"6" => 30,
"7" => 31
);
/**
* Base32 Encoding dictionary
*/
private static $base32_enc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
/**
* Generate Secret Key
* @return string
*/
public static function genKey()
{
// RFC 4226 recommends 160bits Secret Keys, that's 20 Bytes for the lazy ones.
$crypto = false;
$raw = "";
$x = -1;
while ($crypto == false || ++$x < 10) {
$raw = openssl_random_pseudo_bytes(20, $crypto);
}
// RFC 4648 Base32 Encoding without padding
$len = strlen($raw);
$bin = "";
$x = -1;
while (++$x < $len) {
$bin .= str_pad(base_convert(ord($raw[$x]), 10, 2), 8, '0', STR_PAD_LEFT);
}
$bin = str_split($bin, 5);
$ret = "";
$x = -1;
while (++$x < sizeof($bin)) {
$ret .= self::$base32_enc[base_convert(str_pad($bin[$x], 5, '0'), 2, 10)];
}
return $ret;
}
/**
* Return the HTML for the TwoFactor Input-Form
* @param boolean $form_tags Include FORM-tags
* @return string
*/
public static function getForm($form_tags = true)
{
$ret = '';
if ($form_tags) {
$ret .= '
<div class="row">
<div class="col-md-offset-4 col-md-4">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
<img class="img-responsive" src="' . Config::get('title_image') . '">
</h3>
</div>
<div class="panel-body">
<div class="container-fluid">
<form class="form-horizontal" role="form" action="" method="post" name="twofactorform">';
}
$ret .= '
<div class="form-group">
<div class="col-md-12">
<input type="text" name="twofactor" id="twofactor" class="form-control" autocomplete="off" placeholder="Please enter auth token" />
</div>
</div>
<div class="form-group">
<div class="col-md-12">
<button type="submit" class="btn btn-default btn-block" name="submit">Submit</button>
</div>
</div>
</div>';
$ret .= '<script>document.twofactorform.twofactor.focus();</script>';
if ($form_tags) {
$ret .= '</form>';
}
return $ret;
}
/**
* Authenticate with two factor
* Will set $twofactorform if the token hasn't been requested yet (page will redirect to the logon page)
*
* @return bool returns false if the form is not needed
* @throws AuthenticationException
*/
public static function showForm()
{
global $twofactorform;
$twofactor = get_user_pref('twofactor');
// no need to show the form, user doesn't have a token
if (empty($twofactor)) {
$_SESSION['twofactor'] = true;
return false;
}
// lockout the user if there are too many failures
if ($twofactor['fails'] >= 3) {
if (!Config::get('twofactor_lock')) {
throw new AuthenticationException('Too many two-factor failures, please contact administrator.');
} elseif ((time() - $twofactor['last']) < Config::get('twofactor_lock')) {
$msg = "Too many two-factor failures, please wait " . Config::get('twofactor_lock') . " seconds";
throw new AuthenticationException($msg);
}
}
// set $twofactorform to show the form in logon.inc.php
$twofactorform = true;
return true;
}
/**
* Check a 2fa token this will be stored in $_POST['twofactor'] by the form
* If valid, $_SESSION['twofactor'] = true will be set and this will return true
*
* @param string $token The 2fa token, stored in $_POST['twofactor'] by the form
* @return bool If the token was valid
* @throws AuthenticationException Thrown if the token was invalid
*/
public static function authenticate($token)
{
if (!$token) {
throw new AuthenticationException("No Two-Factor Token entered.");
}
$twofactor = get_user_pref('twofactor');
if (empty($twofactor)) {
throw new AuthenticationException('No Two-Factor settings, how did you get here?');
}
if (($server_c = self::verifyHOTP($twofactor['key'], $_POST['twofactor'], $twofactor['counter'])) === false) {
$twofactor['fails']++;
$twofactor['last'] = time();
set_user_pref('twofactor', $twofactor);
throw new AuthenticationException("Wrong Two-Factor Token.");
}
if ($twofactor['counter'] !== false) {
if ($server_c !== true && $server_c !== $twofactor['counter']) {
$twofactor['counter'] = $server_c + 1;
} else {
$twofactor['counter']++;
}
}
$twofactor['fails'] = 0;
set_user_pref('twofactor', $twofactor);
$_SESSION['twofactor'] = true;
return true;
}
/**
* Verify HOTP token honouring window
*
* @param string $key Secret Key
* @param int $otp OTP supplied by user
* @param int|boolean $counter Counter, if false timestamp is used
* @return boolean|int
*/
public static function verifyHOTP($key, $otp, $counter = false)
{
if (self::oathHOTP($key, $counter) == $otp) {
return true;
} else {
if ($counter === false) {
//TimeBased HOTP requires lookbehind and lookahead.
$counter = floor(microtime(true) / self::KEY_INTERVAL);
$initcount = $counter - ((self::OTP_WINDOW + 1) * self::KEY_INTERVAL);
$endcount = $counter + (self::OTP_WINDOW * self::KEY_INTERVAL);
$totp = true;
} else {
//Counter based HOTP only has lookahead, not lookbehind.
$initcount = $counter - 1;
$endcount = $counter + self::OTP_WINDOW;
$totp = false;
}
while (++$initcount <= $endcount) {
if (self::oathHOTP($key, $initcount) == $otp) {
if (!$totp) {
return $initcount;
} else {
return true;
}
}
}
}
return false;
}
/**
* Generate HOTP (RFC 4226)
* @param string $key Secret Key
* @param int|boolean $counter Optional Counter, Defaults to Timestamp
* @return int
*/
private static function oathHOTP($key, $counter = false)
{
if ($counter === false) {
$counter = floor(microtime(true) / self::KEY_INTERVAL);
}
$length = strlen($key);
$x = -1;
$y = $z = 0;
$kbin = "";
while (++$x < $length) {
$y <<= 5;
$y += self::$base32[$key[$x]];
$z += 5;
if ($z >= 8) {
$z -= 8;
$kbin .= chr(($y & (0xFF << $z)) >> $z);
}
}
$hash = hash_hmac('sha1', pack('N*', 0) . pack('N*', $counter), $kbin, true);
$offset = ord($hash[19]) & 0xf;
$truncated = (((ord($hash[$offset + 0]) & 0x7f) << 24) |
((ord($hash[$offset + 1]) & 0xff) << 16) |
((ord($hash[$offset + 2]) & 0xff) << 8) |
(ord($hash[$offset + 3]) & 0xff)) % pow(10, self::OTP_SIZE);
return str_pad($truncated, self::OTP_SIZE, '0', STR_PAD_LEFT);
}
}