diff --git a/2fa.lib.php b/2fa.lib.php new file mode 100644 index 0000000..b644467 --- /dev/null +++ b/2fa.lib.php @@ -0,0 +1,164 @@ +. + * + * PHP Google two-factor authentication module. + * + * See https://www.idontplaydarts.com/2011/07/google-totp-two-factor-authentication-for-php/ + * for more details + * + * @author Phil + **/ + +class Google2FA { + + const keyRegeneration = 30; // Interval between key regeneration + const otpLength = 6; // Length of the Token generated + + private static $lut = array( // Lookup needed for Base32 encoding + "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 + ); + + /** + * Generates a 16 digit secret key in base32 format + * @return string + **/ + public static function generate_secret_key($length = 16) { + $b32 = "234567QWERTYUIOPASDFGHJKLZXCVBNM"; + $s = ""; + + for ($i = 0; $i < $length; $i++) + $s .= $b32[rand(0,31)]; + + return $s; + } + + /** + * Returns the current Unix Timestamp divided by the keyRegeneration + * period. + * @return integer + **/ + public static function get_timestamp() { + return floor(microtime(true)/self::keyRegeneration); + } + + /** + * Decodes a base32 string into a binary string. + **/ + public static function base32_decode($b32) { + + $b32 = strtoupper($b32); + + if (!preg_match('/^[ABCDEFGHIJKLMNOPQRSTUVWXYZ234567]+$/', $b32, $match)) + throw new Exception('Invalid characters in the base32 string.'); + + $l = strlen($b32); + $n = 0; + $j = 0; + $binary = ""; + + for ($i = 0; $i < $l; $i++) { + + $n = $n << 5; // Move buffer left by 5 to make room + $n = $n + self::$lut[$b32[$i]]; // Add value into buffer + $j = $j + 5; // Keep track of number of bits in buffer + + if ($j >= 8) { + $j = $j - 8; + $binary .= chr(($n & (0xFF << $j)) >> $j); + } + } + + return $binary; + } + + /** + * Takes the secret key and the timestamp and returns the one time + * password. + * + * @param binary $key - Secret key in binary form. + * @param integer $counter - Timestamp as returned by get_timestamp. + * @return string + **/ + public static function oath_hotp($key, $counter) + { + if (strlen($key) < 8) + throw new Exception('Secret key is too short. Must be at least 16 base 32 characters'); + + $bin_counter = pack('N*', 0) . pack('N*', $counter); // Counter must be 64-bit int + $hash = hash_hmac ('sha1', $bin_counter, $key, true); + + return str_pad(self::oath_truncate($hash), self::otpLength, '0', STR_PAD_LEFT); + } + + /** + * Verifys a user inputted key against the current timestamp. Checks $window + * keys either side of the timestamp. + * + * @param string $b32seed + * @param string $key - User specified key + * @param integer $window + * @param boolean $useTimeStamp + * @return boolean + **/ + public static function verify_key($b32seed, $key, $window = 0, $useTimeStamp = true) { + + $timeStamp = self::get_timestamp(); + + if ($useTimeStamp !== true) $timeStamp = (int)$useTimeStamp; + + $binarySeed = self::base32_decode($b32seed); + + for ($ts = $timeStamp - $window; $ts <= $timeStamp + $window; $ts++) + if (self::oath_hotp($binarySeed, $ts) == $key) + return true; + + return false; + + } + + /** + * Extracts the OTP from the SHA1 hash. + * @param binary $hash + * @return integer + **/ + public static function oath_truncate($hash) + { + $offset = ord($hash[19]) & 0xf; + + return ( + ((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::otpLength); + } + +} +?> diff --git a/tinyfilemanager.php b/tinyfilemanager.php index 71786c9..f964093 100644 --- a/tinyfilemanager.php +++ b/tinyfilemanager.php @@ -1,6 +1,6 @@ '$2y$10$Fg6Dz8oH9fPoZ2jJan5tZuv6Z4Kp7avtQ9bDfrdRntXtPeiMAZyGO' //12345 ); +// Login 2FA / OTP secrets (login with random code to generate) +$otp_secrets = array(); +$tfa_lib = '2fa.lib.php'; + // Readonly users // e.g. array('users', 'guest', ...) $readonly_users = array( @@ -193,6 +197,8 @@ $report_errors = isset($cfg->data['error_reporting']) ? $cfg->data['error_report // Hide Permissions and Owner cols in file-listing $hide_Cols = isset($cfg->data['hide_Cols']) ? $cfg->data['hide_Cols'] : true; +// Use 2FA authentication for all users +$use_2FA = isset($cfg->data['use_2FA']) ? $cfg->data['use_2FA'] : true; // Theme $theme = isset($cfg->data['theme']) ? $cfg->data['theme'] : 'light'; @@ -326,6 +332,58 @@ if ($use_auth) { sleep(1); if(function_exists('password_verify')) { if (isset($auth_users[$_POST['fm_usr']]) && isset($_POST['fm_pwd']) && password_verify($_POST['fm_pwd'], $auth_users[$_POST['fm_usr']]) && verifyToken($_POST['token'])) { + // Login with 2FA TOTP + if ($use_2FA) { + if (!file_exists($tfa_lib)) { + unset($_SESSION[FM_SESSION_ID]['logged']); + die("Fatal error: Missing 2FA Authentication library: $tfa_lib"); + } + require_once($tfa_lib); + + // Generate random OTP secret, manually add entry inside '$otp_secrets' array + if (!isset($otp_secrets[$_POST['fm_usr']])) { + $QR_onlineAPI = 0; + $random_Base32_InitKey = Google2FA::generate_secret_key(56); + $otp_uri = urlencode("otpauth://totp/TFM:$_POST[fm_usr]@$_SERVER[SERVER_NAME]?secret=$random_Base32_InitKey&issuer=TFM&algorithm=SHA1&digits=6&period=30"); + //$qr_gen_api = "https://api.qrserver.com/v1/create-qr-code/?size=200x200&ecc=L&data="; + $qr_gen_api = "https://chart.googleapis.com/chart?cht=qr&chs=200x200&chld=L|0&chl="; + echo '

New OTP secret generated!

Add the secret below to the $otp_secrets array and scan the QR code to add it to your personal 2FA vault.

'; + echo "'$_POST[fm_usr]' => '$random_Base32_InitKey'


"; + if ($QR_onlineAPI != 0) echo 'QR Code'; + if ($QR_onlineAPI == 0) { + echo ''; + echo '
'; + echo ''; + } + unset($_SESSION[FM_SESSION_ID]['logged']); + exit; + } + + // Retrieve secret for user that successfully logged in + $InitalizationKey = $otp_secrets[$_POST['fm_usr']]; + + // Validate OTP + if (isset($_POST['otp'])) { + if (!Google2FA::verify_key($InitalizationKey, $_POST['otp'])) { + unset($_SESSION[FM_SESSION_ID]['logged']); + fm_set_msg(lng('Login failed. Invalid username or password'), 'error'); + fm_redirect(FM_SELF_URL); + } + } else { + unset($_SESSION[FM_SESSION_ID]['logged']); + fm_set_msg(lng('Login failed. Invalid username or password'), 'error'); + fm_redirect(FM_SELF_URL); + } + } $_SESSION[FM_SESSION_ID]['logged'] = $_POST['fm_usr']; fm_set_msg(lng('You are logged in')); fm_redirect(FM_SELF_URL); @@ -373,6 +431,13 @@ if ($use_auth) { + +
+ + +
+ +
@@ -525,7 +590,7 @@ if ((isset($_SESSION[FM_SESSION_ID]['logged'], $auth_users[$_SESSION[FM_SESSION_ // Save Config if (isset($_POST['type']) && $_POST['type'] == "settings") { - global $cfg, $lang, $report_errors, $show_hidden_files, $lang_list, $hide_Cols, $theme; + global $cfg, $lang, $report_errors, $show_hidden_files, $lang_list, $hide_Cols, $use_2FA, $theme; $newLng = $_POST['js-language']; fm_get_translations([]); if (!array_key_exists($newLng, $lang_list)) { @@ -535,6 +600,7 @@ if ((isset($_SESSION[FM_SESSION_ID]['logged'], $auth_users[$_SESSION[FM_SESSION_ $erp = isset($_POST['js-error-report']) && $_POST['js-error-report'] == "true" ? true : false; $shf = isset($_POST['js-show-hidden']) && $_POST['js-show-hidden'] == "true" ? true : false; $hco = isset($_POST['js-hide-cols']) && $_POST['js-hide-cols'] == "true" ? true : false; + $tfa = isset($_POST['js-use-2FA']) && $_POST['js-use-2FA'] == "true" ? true : false; $te3 = $_POST['js-theme-3']; if ($cfg->data['lang'] != $newLng) { @@ -557,6 +623,10 @@ if ((isset($_SESSION[FM_SESSION_ID]['logged'], $auth_users[$_SESSION[FM_SESSION_ $cfg->data['hide_Cols'] = $hco; $hide_Cols = $hco; } + if ($cfg->data['use_2FA'] != $tfa) { + $cfg->data['use_2FA'] = $tfa; + $use_2FA = $tfa; + } if ($cfg->data['theme'] != $te3) { $cfg->data['theme'] = $te3; $theme = $te3; @@ -1558,6 +1628,17 @@ if (isset($_GET['settings']) && !FM_READONLY) { + +
+ +
+
+ /> +
+
+
+ +