mirror of
https://github.com/librenms/librenms.git
synced 2024-10-07 16:52:45 +00:00
Tested against Google-Authenticator app on Android 4.4.4 Made `verify_hotp` more efficient. Added autofocus on twofactor input Added GUI Unlock and Remove for TwoFactor credentials in /edituser/ Allow additional tries after elapsed time from last try exceeds configured parameter `$config['twofactor_lock']`. If `$config['twofactor_lock']` is not defined or is set to `0`, administrators have to unlock accounts that exceed 3 failures via GUI. Added Documentation Moved TwoFactor form to logon.inc.php Disabled autocomplete on twofactor input field Updated Docs to include link to Google-Authenticator's install-guides Moved authentication logic from authenticate.inc.php to twofactor.lib.php typo in docblock for `twofactor_auth()` Fixed scrutinizer bugs To please scrutinizer
232 lines
6.6 KiB
PHP
232 lines
6.6 KiB
PHP
<?php
|
|
/* Copyright (C) 2014 Daniel Preussker <f0o@devilcode.org>
|
|
* 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/>. */
|
|
|
|
/**
|
|
* Two-Factor Authentication Library
|
|
* @author f0o <f0o@devilcode.org>
|
|
* @copyright 2014 f0o, LibreNMS
|
|
* @license GPL
|
|
* @package LibreNMS
|
|
* @subpackage Authentication
|
|
*/
|
|
|
|
/**
|
|
* Key Interval in seconds.
|
|
* Set to 30s due to Google-Authenticator limitation.
|
|
* Sadly Google-Auth is the most used Non-Physical OTP app.
|
|
*/
|
|
const keyInterval = 30;
|
|
|
|
/**
|
|
* Size of the OTP.
|
|
* Set to 6 for the same reasons as above.
|
|
*/
|
|
const otpSize = 6;
|
|
|
|
/**
|
|
* Window to honour whilest verifying OTP.
|
|
*/
|
|
const otpWindow = 4;
|
|
|
|
/**
|
|
* Base32 Decoding dictionary
|
|
*/
|
|
$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
|
|
*/
|
|
$base32_enc = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
|
|
/**
|
|
* Generate Secret Key
|
|
* @return string
|
|
*/
|
|
function twofactor_genkey() {
|
|
global $base32_enc;
|
|
// 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 .= $base32_enc[base_convert(str_pad($bin[$x], 5, '0'), 2, 10)];
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Generate HOTP (RFC 4226)
|
|
* @param string $key Secret Key
|
|
* @param int|boolean $counter Optional Counter, Defaults to Timestamp
|
|
* @return int
|
|
*/
|
|
function oath_hotp($key, $counter=false) {
|
|
global $base32;
|
|
if( $counter === false ) {
|
|
$counter = floor(microtime(true)/keyInterval);
|
|
}
|
|
$length = strlen($key);
|
|
$x = -1;
|
|
$y = $z = 0;
|
|
$kbin = "";
|
|
while( ++$x < $length ) {
|
|
$y <<= 5;
|
|
$y += $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, otpSize);
|
|
return str_pad($truncated, otpSize, '0', STR_PAD_LEFT);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function verify_hotp($key,$otp,$counter=false) {
|
|
if( oath_hotp($key,$counter) == $otp ) {
|
|
return true;
|
|
} else {
|
|
if( $counter === false ) {
|
|
//TimeBased HOTP requires lookbehind and lookahead.
|
|
$counter = floor(microtime(true)/keyInterval);
|
|
$initcount = $counter-((otpWindow+1)*keyInterval);
|
|
$endcount = $counter+(otpWindow*keyInterval);
|
|
$totp = true;
|
|
} else {
|
|
//Counter based HOTP only has lookahead, not lookbehind.
|
|
$initcount = $counter-1;
|
|
$endcount = $counter+otpWindow;
|
|
$totp = false;
|
|
}
|
|
while( ++$initcount <= $endcount ) {
|
|
if( oath_hotp($key,$initcount) == $otp ) {
|
|
if( !$totp ) {
|
|
return $initcount;
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Print TwoFactor Input-Form
|
|
* @param boolean $form Include FORM-tags
|
|
* @return void|string
|
|
*/
|
|
function twofactor_form($form=true){
|
|
global $config;
|
|
$ret = "";
|
|
if( $form ) {
|
|
$ret .= '
|
|
<form class="form-horizontal" role="form" action="" method="post" name="twofactorform">';
|
|
}
|
|
$ret .= '
|
|
<div class="form-group">
|
|
<div class="col-sm-offset-2 col-sm-10">
|
|
<h3>Please Enter TwoFactor Token:</h3>
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="twofactor" class="col-sm-2 control-label">Token</label>
|
|
<div class="col-sm-6">
|
|
<input type="text" name="twofactor" id="twofactor" class="form-control" autocomplete="off" placeholder="012345" />
|
|
</div>
|
|
</div>
|
|
<div class="form-group">
|
|
<div class="col-sm-offset-2 col-sm-6">
|
|
<button type="submit" class="btn btn-default input-sm" name="submit" type="submit">Login</button>
|
|
</div>
|
|
</div>';
|
|
$ret .= '<script>document.twofactorform.twofactor.focus();</script>';
|
|
if( $form ) {
|
|
$ret .= '
|
|
</form>';
|
|
}
|
|
return $ret;
|
|
}
|
|
|
|
/**
|
|
* Authentication logic
|
|
* @return void
|
|
*/
|
|
function twofactor_auth() {
|
|
global $auth_message, $twofactorform, $config;
|
|
$twofactor = dbFetchRow('SELECT twofactor FROM users WHERE username = ?', array($_SESSION['username']));
|
|
if( empty($twofactor['twofactor']) ) {
|
|
$_SESSION['twofactor'] = true;
|
|
} else {
|
|
$twofactor = json_decode($twofactor['twofactor'],true);
|
|
if( $twofactor['fails'] >= 3 && (!$config['twofactor_lock'] || (time()-$twofactor['last']) < $config['twofactor_lock']) ) {
|
|
$auth_message = "Too many failures, please ".($config['twofactor_lock'] ? "wait ".$config['twofactor_lock']." seconds" : "contact administrator").".";
|
|
} else {
|
|
if( !$_POST['twofactor'] ) {
|
|
$twofactorform = true;
|
|
} else {
|
|
if( ($server_c = verify_hotp($twofactor['key'],$_POST['twofactor'],$twofactor['counter'])) === false ) {
|
|
$twofactor['fails']++;
|
|
$twofactor['last'] = time();
|
|
$auth_message = "Wrong Two-Factor Token.";
|
|
} else {
|
|
if( $twofactor['counter'] !== false ) {
|
|
if( $server_c !== true && $server_c !== $twofactor['counter'] ) {
|
|
$twofactor['counter'] = $server_c+1;
|
|
} else {
|
|
$twofactor['counter']++;
|
|
}
|
|
}
|
|
$twofactor['fails'] = 0;
|
|
$_SESSION['twofactor'] = true;
|
|
}
|
|
dbUpdate(array('twofactor' => json_encode($twofactor)),'users','username = ?',array($_SESSION['username']));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
?>
|