Files
librenms-librenms/LibreNMS/Authentication/TwoFactor.php

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

217 lines
6.0 KiB
PHP
Raw Normal View History

<?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 <https://www.gnu.org/licenses/>.
*
* @license GPL
2021-09-10 20:09:53 +02:00
*
* @link https://www.librenms.org
2021-09-10 20:09:53 +02:00
*
* @author f0o <f0o@devilcode.org>
* @copyright 2014 f0o, LibreNMS
* @copyright 2017 Tony Murray
*/
namespace LibreNMS\Authentication;
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 = [
'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
2021-09-10 20:09:53 +02:00
*
* @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) {
Update dependencies (#14319) * Update dependencies hpdocumentor/reflection-docblock (5.3.0) hpspec/prophecy (v1.15.0) ymfony/debug (v4.4.41) barryvdh/laravel-debugbar (v3.6.7 => v3.7.0) composer/ca-bundle (1.3.2 => 1.3.3) composer/class-map-generator (1.0.0) composer/composer (2.3.7 => 2.4.1) doctrine/annotations (1.13.2 => 1.13.3) doctrine/event-manager (1.1.1 => 1.1.2) doctrine/inflector (2.0.4 => 2.0.5) fakerphp/faker (v1.19.0 => v1.20.0) graham-campbell/result-type (v1.0.4 => v1.1.0) guzzlehttp/guzzle (7.4.5 => 7.5.0) guzzlehttp/promises (1.5.1 => 1.5.2) guzzlehttp/psr7 (2.4.0 => 2.4.1) laravel/dusk (v6.24.0 => v6.25.1) laravel/framework (v8.83.16 => v8.83.23) laravel/serializable-closure (v1.2.0 => v1.2.1) laravel/socialite (v5.5.2 => v5.5.5) maximebf/debugbar (v1.18.0 => v1.18.1) mews/purifier (3.3.7 => 3.3.8) mockery/mockery (1.5.0 => 1.5.1) monolog/monolog (2.7.0 => 2.8.0) nesbot/carbon (2.58.0 => 2.62.1) nikic/php-parser (v4.14.0 => v4.15.1) paragonie/constant_time_encoding (v2.6.1 => v2.6.3) phpmailer/phpmailer (v6.6.0 => v6.6.4) phpoption/phpoption (1.8.1 => 1.9.0) phpseclib/phpseclib (3.0.14 => 3.0.16) phpstan/phpstan (1.7.12 => 1.8.5) phpunit/php-code-coverage (9.2.15 => 9.2.17) phpunit/phpunit (9.5.20 => 9.5.24) psy/psysh (v0.11.5 => v0.11.8) sebastian/type (3.0.0 => 3.1.0) seld/phar-utils (1.2.0 => 1.2.1) seld/signal-handler (2.0.1) symfony/console (v5.4.9 => v5.4.12) symfony/css-selector (v5.4.3 => v5.4.11) symfony/deprecation-contracts (v2.5.1 => v2.5.2) symfony/error-handler (v5.4.9 => v5.4.11) symfony/event-dispatcher-contracts (v2.5.1 => v2.5.2) symfony/filesystem (v5.4.9 => v5.4.12) symfony/finder (v5.4.8 => v5.4.11) symfony/http-foundation (v5.4.9 => v5.4.12) symfony/http-kernel (v5.4.9 => v5.4.12) symfony/mime (v5.4.9 => v5.4.12) symfony/options-resolver (v5.4.3 => v5.4.11) symfony/process (v5.4.8 => v5.4.11) symfony/routing (v5.4.8 => v5.4.11) symfony/service-contracts (v2.5.1 => v2.5.2) symfony/string (v5.4.9 => v5.4.12) symfony/translation (v5.4.9 => v5.4.12) symfony/translation-contracts (v2.5.1 => v2.5.2) symfony/var-dumper (v5.4.9 => v5.4.11) symfony/yaml (v4.4.37 => v4.4.45) tecnickcom/tcpdf (6.4.4 => 6.5.0) * changes * try again * Fix some issues because the message is changing between versions, just avoids it.
2022-09-09 09:55:59 -05:00
$bin .= str_pad(base_convert((string) ord($raw[$x]), 10, 2), 8, '0', STR_PAD_LEFT);
}
$bin = str_split($bin, 5);
$ret = '';
$x = -1;
while (++$x < count($bin)) {
Update dependencies (#14319) * Update dependencies hpdocumentor/reflection-docblock (5.3.0) hpspec/prophecy (v1.15.0) ymfony/debug (v4.4.41) barryvdh/laravel-debugbar (v3.6.7 => v3.7.0) composer/ca-bundle (1.3.2 => 1.3.3) composer/class-map-generator (1.0.0) composer/composer (2.3.7 => 2.4.1) doctrine/annotations (1.13.2 => 1.13.3) doctrine/event-manager (1.1.1 => 1.1.2) doctrine/inflector (2.0.4 => 2.0.5) fakerphp/faker (v1.19.0 => v1.20.0) graham-campbell/result-type (v1.0.4 => v1.1.0) guzzlehttp/guzzle (7.4.5 => 7.5.0) guzzlehttp/promises (1.5.1 => 1.5.2) guzzlehttp/psr7 (2.4.0 => 2.4.1) laravel/dusk (v6.24.0 => v6.25.1) laravel/framework (v8.83.16 => v8.83.23) laravel/serializable-closure (v1.2.0 => v1.2.1) laravel/socialite (v5.5.2 => v5.5.5) maximebf/debugbar (v1.18.0 => v1.18.1) mews/purifier (3.3.7 => 3.3.8) mockery/mockery (1.5.0 => 1.5.1) monolog/monolog (2.7.0 => 2.8.0) nesbot/carbon (2.58.0 => 2.62.1) nikic/php-parser (v4.14.0 => v4.15.1) paragonie/constant_time_encoding (v2.6.1 => v2.6.3) phpmailer/phpmailer (v6.6.0 => v6.6.4) phpoption/phpoption (1.8.1 => 1.9.0) phpseclib/phpseclib (3.0.14 => 3.0.16) phpstan/phpstan (1.7.12 => 1.8.5) phpunit/php-code-coverage (9.2.15 => 9.2.17) phpunit/phpunit (9.5.20 => 9.5.24) psy/psysh (v0.11.5 => v0.11.8) sebastian/type (3.0.0 => 3.1.0) seld/phar-utils (1.2.0 => 1.2.1) seld/signal-handler (2.0.1) symfony/console (v5.4.9 => v5.4.12) symfony/css-selector (v5.4.3 => v5.4.11) symfony/deprecation-contracts (v2.5.1 => v2.5.2) symfony/error-handler (v5.4.9 => v5.4.11) symfony/event-dispatcher-contracts (v2.5.1 => v2.5.2) symfony/filesystem (v5.4.9 => v5.4.12) symfony/finder (v5.4.8 => v5.4.11) symfony/http-foundation (v5.4.9 => v5.4.12) symfony/http-kernel (v5.4.9 => v5.4.12) symfony/mime (v5.4.9 => v5.4.12) symfony/options-resolver (v5.4.3 => v5.4.11) symfony/process (v5.4.8 => v5.4.11) symfony/routing (v5.4.8 => v5.4.11) symfony/service-contracts (v2.5.1 => v2.5.2) symfony/string (v5.4.9 => v5.4.12) symfony/translation (v5.4.9 => v5.4.12) symfony/translation-contracts (v2.5.1 => v2.5.2) symfony/var-dumper (v5.4.9 => v5.4.11) symfony/yaml (v4.4.37 => v4.4.45) tecnickcom/tcpdf (6.4.4 => 6.5.0) * changes * try again * Fix some issues because the message is changing between versions, just avoids it.
2022-09-09 09:55:59 -05:00
$ret .= self::$base32_enc[(int) base_convert(str_pad($bin[$x], 5, '0'), 2, 10)];
}
return $ret;
}
/**
* Verify HOTP token honouring window
*
2021-09-08 23:35:56 +02:00
* @param string $key Secret Key
* @param int $otp OTP supplied by user
* @param int|bool $counter Counter, if false timestamp is used
* @return bool|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)
2021-09-10 20:09:53 +02:00
*
2021-09-08 23:35:56 +02:00
* @param string $key Secret Key
* @param int|bool $counter Optional Counter, Defaults to Timestamp
2021-04-01 00:35:19 +02:00
* @return string
*/
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);
2021-09-08 03:33:54 +02:00
$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);
2023-04-15 09:02:41 -05:00
return str_pad("$truncated", self::OTP_SIZE, '0', STR_PAD_LEFT);
}
/**
* Generate 2fa URI
2021-09-10 20:09:53 +02:00
*
2021-09-08 23:35:56 +02:00
* @param string $username
* @param string $key
* @param bool $counter if type is counter (false for time based)
* @return string
*/
public static function generateUri($username, $key, $counter = false)
{
$title = 'LibreNMS:' . urlencode($username);
return $counter ?
"otpauth://hotp/$title?issuer=LibreNMS&counter=1&secret=$key" : // counter based
"otpauth://totp/$title?issuer=LibreNMS&secret=$key"; // time based
}
}