Added TwoFactor Authentication (RFC4226)

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
This commit is contained in:
f0o
2014-12-24 21:22:02 +00:00
parent 7dccc13a6c
commit d66cec7017
8 changed files with 472 additions and 5 deletions

72
doc/TwoFactor.md Normal file
View File

@ -0,0 +1,72 @@
Table of Content:
- [About](#about)
- [Types](#types)
- [Timebased One-Time-Password (TOTP)](#totp)
- [Counterbased One-Time-Password (HOTP)](#hotp)
- [Configuration](#config)
- [Usage](#usage)
- [Google Authenticator](#usage-google)
# <a name="about">About</a>
Over the last couple of years, the primary attack vector for internet accounts has been static passwords.
Therefore static passwords are no longer suffient to protect unauthorized access to accounts.
Two Factor Authentication adds a variable part in authentication procedures.
A user is now required to supply a changing 6-digit passcode in addition to it's password to obtain access to the account.
LibreNMS has a RFC4226 conform implementation of both Time and Counter based One-Time-Passwords.
It also allows the administrator to configure a throttle time to enforce after 3 failures exceeded. Unlike RFC4226 suggestions, this throttle time will not stack on the amount of failures.
# <a name="types">Types</a>
In general, these two types do not differ in algorithmic terms.
The types only differ in the variable being used to derive the passcodes from.
The underlying HMAC-SHA1 remains the same for both types, security advantages or disadvantages of each are discussed further down.
## <a name="totp">Timebased One-Time-Password (TOTP)</a>
Like the name suggests, this type uses the current Time or a subset of it to generate the passcodes.
These passcodes solely rely on the secrecy of their Secretkey in order to provide passcodes.
An attacker only needs to guess that Secretkey and the other variable part is any given time, presumably the time upon login.
RFC4226 suggests a resynchronization attempt in case the passcode mismatches, providing the attacker a range of upto +/- 3 Minutes to create passcodes.
## <a name="hotp">Counterbased One-Time-Password (TOTP)</a>
This type uses an internal counter that needs to be in-synch with the server's counter to successfully authenticate the passcodes.
The main advantage over timebased OTP is the attacker doesnt only need to know the Secretkey but also the server's Counter in order to create valid passcodes.
RFC4226 suggests a resynchronization attempt in case the passcode mismatches, providing the attacker a range of upto +4 increments from the actual counter to create passcodes.
# <a name="config">Configuration</a>
Enable Two-Factor:
```php
$config['twofactor'] = true;
```
Set throttle-time (in secconds):
```php
$config['twofactor_lock'] = 300;
```
# <a name="usage">Usage</a>
These steps imply that TwoFactor has been enabled in your `config.php`
Create a Two-Factor key:
- Go to 'My Settings' (/preferences/)
- Choose TwoFactor type
- Click on 'Generate TwoFactor Secret Key'
- If your browser didnt reload, reload manually
- Scan provided QR or click on 'Manual' to see the Key
## <a name="usage-google">Google Authenticator</a>
__Note__: Google Authenticator only allows counterbased OTP when scanned via QR codes.
Installation guides for Google Authneticator can be found [here](https://support.google.com/accounts/answer/1066447?hl=en).
Usage:
- Create a key like described above
- Scan provided QR or click on 'Manual' and type down the Secret
- On next login, enter the passcode that the App provides

View File

@ -75,10 +75,16 @@ if ((isset($_SESSION['username'])) || (isset($_COOKIE['sess_id'],$_COOKIE['token
$_SESSION['user_id'] = get_userid($_SESSION['username']);
if (!$_SESSION['authenticated'])
{
if( $config['twofactor'] === true && !isset($_SESSION['twofactor']) ) {
require_once($config['install_dir'].'/html/includes/authentication/twofactor.lib.php');
twofactor_auth();
}
if( !$config['twofactor'] || $_SESSION['twofactor'] ) {
$_SESSION['authenticated'] = true;
dbInsert(array('user' => $_SESSION['username'], 'address' => $_SERVER["REMOTE_ADDR"], 'result' => 'Logged In'), 'authlog');
header("Location: ".$_SERVER['REQUEST_URI']);
}
}
if (isset($_POST['remember']))
{
$sess_id = session_id();

View File

@ -0,0 +1,231 @@
<?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']));
}
}
}
}
?>

2
html/js/jquery.qrcode.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -270,6 +270,26 @@ if ($_SESSION['userlevel'] != '10') { include("includes/error-no-perm.inc.php");
$vars['new_email'] = $users_details['email'];
}
if( $config['twofactor'] ) {
if( $vars['twofactorremove'] ) {
if( dbUpdate(array('twofactor'=>''),users,'user_id = ?',array($vars['user_id'])) ) {
echo "<div class='alert alert-success'>TwoFactor credentials removed.</div>";
} else {
echo "<div class='alert alert-danger'>Couldnt remove user's TwoFactor credentials.</div>";
}
}
if( $vars['twofactorunlock'] ) {
$twofactor = dbFetchRow("SELECT twofactor FROM users WHERE user_id = ?",array($vars['user_id']));
$twofactor = json_decode($twofactor['twofactor'],true);
$twofactor['fails'] = 0;
if( dbUpdate(array('twofactor'=>json_encode($twofactor)),users,'user_id = ?',array($vars['user_id'])) ) {
echo "<div class='alert alert-success'>User unlocked.</div>";
} else {
echo "<div class='alert alert-danger'>Couldnt reset user's TwoFactor failures.</div>";
}
}
}
echo("<form class='form-horizontal' role='form' method='post' action=''>
<input type='hidden' name='user_id' value='" . $vars['user_id'] . "'>
<input type='hidden' name='edit' value='yes'>
@ -314,6 +334,33 @@ if ($_SESSION['userlevel'] != '10') { include("includes/error-no-perm.inc.php");
</div>
<button type='submit' class='btn btn-default'>Update User</button>
</form>");
if( $config['twofactor'] ) {
echo "<br/><div class='well'><h3>Two-Factor Authentication</h3>";
$twofactor = dbFetchRow("SELECT twofactor FROM users WHERE user_id = ?",array($vars['user_id']));
$twofactor = json_decode($twofactor['twofactor'],true);
if( $twofactor['fails'] >= 3 && (!$config['twofactor_lock'] || (time()-$twofactor['last']) < $config['twofactor_lock']) ) {
echo "<form class='form-horizontal' role='form' method='post' action=''>
<input type='hidden' name='user_id' value='" . $vars['user_id'] . "'>
<input type='hidden' name='edit' value='yes'>
<div class='form-group'>
<label for='twofactorunlock' class='col-sm-2 control-label'>User exceeded failures</label>
<input type='hidden' name='twofactorunlock' value='1'>
<button type='submit' class='btn btn-default'>Unlock</button>
</div>
</form>";
}
if( $twofactor['key'] ) {
echo "<form class='form-horizontal' role='form' method='post' action=''>
<input type='hidden' name='user_id' value='" . $vars['user_id'] . "'>
<input type='hidden' name='edit' value='yes'>
<input type='hidden' name='twofactorremove' value='1'>
<button type='submit' class='btn btn-danger'>Disable TwoFactor</button>
</form>
</div>";
} else {
echo "<p>No TwoFactor key generated for this user, Nothing to do.</p>";
}
}
} else {
echo print_error("Error getting user details");
}

View File

@ -1,3 +1,7 @@
<?php
if( $config['twofactor'] && isset($twofactorform) ) {
echo twofactor_form();
} else { ?>
<form class="form-horizontal" role="form" action="" method="post" name="logonform">
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
@ -56,6 +60,8 @@ if (isset($config['login_message']))
document.logonform.username.focus();
// -->
</script>
<?php
}
?>
</div>
</div>

View File

@ -65,6 +65,108 @@ if (passwordscanchange($_SESSION['username']))
echo("</div>");
}
if( $config['twofactor'] === true ) {
if( $_POST['twofactorremove'] == 1 ) {
require_once($config['install_dir']."/html/includes/authentication/twofactor.lib.php");
if( !isset($_POST['twofactor']) ) {
echo '<div class="well"><form class="form-horizontal" role="form" action="" method="post" name="twofactorform">';
echo '<input type="hidden" name="twofactorremove" value="1" />';
echo twofactor_form(false);
echo '</form></div>';
} else{
$twofactor = dbFetchRow('SELECT twofactor FROM users WHERE username = ?', array($_SESSION['username']));
if( empty($twofactor['twofactor']) ) {
echo '<div class="alert alert-danger">Error: How did you even get here?!</div><script>window.location = "/preferences/";</script>';
} else {
$twofactor = json_decode($twofactor['twofactor'],true);
}
if( verify_hotp($twofactor['key'],$_POST['twofactor'],$twofactor['counter']) ) {
if( !dbUpdate(array('twofactor' => ''),'users','username = ?',array($_SESSION['username'])) ) {
echo '<div class="alert alert-danger">Error while disabling TwoFactor.</div>';
} else {
echo '<div class="alert alert-success">TwoFactor Disabled.</div>';
}
} else {
session_destroy();
echo '<div class="alert alert-danger">Error: Supplied TwoFactor Token is wrong, you\'ve been logged out.</div><script>window.location = "/";</script>';
}
}
} else {
$twofactor = dbFetchRow("SELECT twofactor FROM users WHERE username = ?", array($_SESSION['username']));
echo '<script src="/js/jquery.qrcode.min.js"></script>';
echo '<div class="well"><h3>Two-Factor Authentication</h3>';
if( !empty($twofactor['twofactor']) ) {
$twofactor = json_decode($twofactor['twofactor'],true);
$twofactor['text'] = "<div class='form-group'>
<label for='twofactorkey' class='col-sm-2 control-label'>Secret Key</label>
<div class='col-sm-4'>
<input type='text' name='twofactorkey' autocomplete='off' disabled class='form-control input-sm' value='".$twofactor['key']."' />
</div>
</div>";
if( $twofactor['counter'] !== false ) {
$twofactor['uri'] = "otpauth://hotp/".$_SESSION['username']."?issuer=LibreNMS&counter=".$twofactor['counter']."&secret=".$twofactor['key'];
$twofactor['text'] .= "<div class='form-group'>
<label for='twofactorcounter' class='col-sm-2 control-label'>Counter</label>
<div class='col-sm-4'>
<input type='text' name='twofactorcounter' autocomplete='off' disabled class='form-control input-sm' value='".$twofactor['counter']."' />
</div>
</div>";
} else {
$twofactor['uri'] = "otpauth://totp/".$_SESSION['username']."?issuer=LibreNMS&secret=".$twofactor['key'];
}
echo '<div id="twofactorqrcontainer">
<div id="twofactorqr"></div>
<button class="btn btn-default" onclick="$(\'#twofactorkeycontainer\').show(); $(\'#twofactorqrcontainer\').hide();">Manual</button>
</div>';
echo '<div id="twofactorkeycontainer">
<form id="twofactorkey" class="form-horizontal" role="form">'.$twofactor['text'].'</form>
<button class="btn btn-default" onclick="$(\'#twofactorkeycontainer\').hide(); $(\'#twofactorqrcontainer\').show();">QR</button>
</div>';
echo '<script>$("#twofactorqr").qrcode({"text": "'.$twofactor['uri'].'"}); $("#twofactorkeycontainer").hide();</script>';
echo '<br/><form method="post" class="form-horizontal" role="form">
<input type="hidden" name="twofactorremove" value="1" />
<button class="btn btn-danger" type="submit">Disable TwoFactor</button>
</form>';
} else {
if( isset($_POST['gentwofactorkey']) && isset($_POST['twofactortype']) ) {
require_once($config['install_dir']."/html/includes/authentication/twofactor.lib.php");
$chk = dbFetchRow("SELECT twofactor FROM users WHERE username = ?", array($_SESSION['username']));
if( empty($chk['twofactor']) ) {
$twofactor = array('key' => twofactor_genkey());
if( $_POST['twofactortype'] == "counter" ) {
$twofactor['counter'] = 1;
} else {
$twofactor['counter'] = false;
}
if( !dbUpdate(array('twofactor' => json_encode($twofactor)),'users','username = ?',array($_SESSION['username'])) ) {
echo '<div class="alert alert-danger">Error inserting TwoFactor details. Please try again later and contact Administrator if error persists.</div>';
} else {
echo '<div class="alert alert-success">Added TwoFactor credentials. Please reload page.</div><script>window.location = "/preferences/";</script>';
}
} else {
echo '<div class="alert alert-danger">TwoFactor credentials already exists.</div>';
}
} else {
echo '<form method="post" class="form-horizontal" role="form">
<input type="hidden" name="gentwofactorkey" value="1" />
<div class="form-group">
<label for="twofactortype" class="col-sm-2 control-label">TwoFactor Type</label>
<div class="col-sm-4">
<select name="twofactortype">
<option value=""></option>
<option value="counter">Counter Based (HOTP)</option>
<option value="time">Time Based (TOTP)</option>
</select>
</div>
</div>
<button class="btn btn-default" type="submit">Generate TwoFactor Secret Key</button>
</form>';
}
}
echo '</div>';
}
}
echo("<div style='background-color: #e5e5e5; border: solid #e5e5e5 10px; margin-bottom:10px;'>");
echo("<div style='font-size: 18px; font-weight: bold; margin-bottom: 5px;'>Device Permissions</div>");

1
sql-schema/038.sql Normal file
View File

@ -0,0 +1 @@
ALTER TABLE `users` ADD `twofactor` VARCHAR( 255 ) NOT NULL;