| 1: | <?php |
| 2: | use BaconQrCode\Renderer\ImageRenderer; |
| 3: | use BaconQrCode\Renderer\Image\SvgImageBackEnd; |
| 4: | use BaconQrCode\Renderer\RendererStyle\RendererStyle; |
| 5: | use BaconQrCode\Writer; |
| 6: | |
| 7: | |
| 8: | |
| 9: | |
| 10: | |
| 11: | |
| 12: | |
| 13: | if (!defined('DEBUG_MODE')) { die(); } |
| 14: | |
| 15: | |
| 16: | |
| 17: | |
| 18: | class Hm_Handler_process_enable_2fa extends Hm_Handler_Module { |
| 19: | public function process() { |
| 20: | function enable_2fa_callback($val) { return $val; } |
| 21: | function backup_2fa_callback($val) { return $val; } |
| 22: | process_site_setting('2fa_enable', $this, 'enable_2fa_callback', false, true); |
| 23: | if (array_key_exists('2fa_enable', $this->request->post) && $this->request->post['2fa_enable']) { |
| 24: | process_site_setting('2fa_backup_codes', $this, 'backup_2fa_callback', false, false); |
| 25: | $this->session->set('2fa_confirmed', true); |
| 26: | } |
| 27: | list($secret, $simple) = get_2fa_key($this->config); |
| 28: | if ($secret) { |
| 29: | if ($simple) { |
| 30: | $len = 15; |
| 31: | } |
| 32: | else { |
| 33: | $len = 64; |
| 34: | } |
| 35: | $username = $this->session->get('username', false); |
| 36: | $secret = base32_encode_str(create_secret($secret, $username, $len)); |
| 37: | $app_name = $this->config->get('app_name', 'Cypht'); |
| 38: | $uri = sprintf('otpauth://totp/%s:%s?secret=%s&issuer=%s', $app_name, $username, $secret, $app_name); |
| 39: | $this->out('2fa_svg', generate_qr_code($this->config, $username, $uri)); |
| 40: | $this->out('2fa_backup_codes', backup_codes($this->user_config)); |
| 41: | $this->out('2fa_secret', $secret); |
| 42: | } |
| 43: | } |
| 44: | } |
| 45: | |
| 46: | |
| 47: | |
| 48: | |
| 49: | class Hm_Handler_2fa_check extends Hm_Handler_Module { |
| 50: | public function process() { |
| 51: | |
| 52: | $enabled = $this->user_config->get('2fa_enable_setting', 0); |
| 53: | if (!$enabled) { |
| 54: | return; |
| 55: | } |
| 56: | |
| 57: | |
| 58: | |
| 59: | |
| 60: | |
| 61: | |
| 62: | list($secret, $simple) = get_2fa_key($this->config); |
| 63: | if (!$secret) { |
| 64: | Hm_Debug::add('2FA module set enabled, but no shared secret configured', 'warning'); |
| 65: | return; |
| 66: | } |
| 67: | |
| 68: | $confirmed = $this->session->get('2fa_confirmed', false); |
| 69: | if ($confirmed) { |
| 70: | return; |
| 71: | } |
| 72: | |
| 73: | $old_setting = $this->user_config->get('enable_2fa_setting', 0); |
| 74: | if ($old_setting && $this->session->loaded) { |
| 75: | Hm_Msgs::add('2FA disabled because of a security issue. Go to "Settings" -> "Site" to re-enable', 'warning'); |
| 76: | } |
| 77: | $passed = false; |
| 78: | $backup_codes = $this->user_config->get('2fa_backup_codes_setting', array()); |
| 79: | if (array_key_exists('2fa_code', $this->request->post)) { |
| 80: | if ($simple) { |
| 81: | $len = 15; |
| 82: | } |
| 83: | else { |
| 84: | $len = 64; |
| 85: | } |
| 86: | $username = $this->session->get('username', false); |
| 87: | $secret = create_secret($secret, $username, $len); |
| 88: | if (check_2fa_pin($this->request->post['2fa_code'], $secret)) { |
| 89: | $passed = true; |
| 90: | } |
| 91: | elseif (in_array(intval($this->request->post['2fa_code']), $backup_codes, true)) { |
| 92: | $passed = true; |
| 93: | } |
| 94: | else { |
| 95: | $this->out('2fa_error', '2 factor authentication code does not match'); |
| 96: | } |
| 97: | } |
| 98: | |
| 99: | if (!$passed) { |
| 100: | $this->out('no_redirect', true); |
| 101: | Hm_Request_Key::load($this->session, $this->request, false); |
| 102: | $this->out('2fa_key', Hm_Request_Key::generate()); |
| 103: | $this->out('2fa_required', true); |
| 104: | $this->session->close_early(); |
| 105: | } |
| 106: | else { |
| 107: | $this->session->set('2fa_confirmed', true); |
| 108: | } |
| 109: | } |
| 110: | } |
| 111: | |
| 112: | |
| 113: | |
| 114: | |
| 115: | |
| 116: | class Hm_Handler_2fa_setup_check extends Hm_Handler_Module { |
| 117: | public function process() { |
| 118: | |
| 119: | list($secret, $simple) = get_2fa_key($this->config); |
| 120: | if (!$secret) { |
| 121: | Hm_Debug::add('2FA module set enabled, but no shared secret configured', 'warning'); |
| 122: | return; |
| 123: | } |
| 124: | |
| 125: | $verified = false; |
| 126: | $len = $simple ? 15 : 64; |
| 127: | |
| 128: | $username = $this->session->get('username', false); |
| 129: | $secret = create_secret($secret, $username, $len); |
| 130: | |
| 131: | if (check_2fa_pin($this->request->post['2fa_code'], $secret)) { |
| 132: | $verified = true; |
| 133: | } |
| 134: | |
| 135: | $this->out('ajax_2fa_verified', $verified); |
| 136: | } |
| 137: | } |
| 138: | |
| 139: | |
| 140: | |
| 141: | |
| 142: | class Hm_Output_enable_2fa_setting extends Hm_Output_Module { |
| 143: | protected function output() { |
| 144: | $enabled = false; |
| 145: | $backup_codes = $this->get('2fa_backup_codes', array()); |
| 146: | $settings = $this->get('user_settings', array()); |
| 147: | if (array_key_exists('2fa_enable', $settings)) { |
| 148: | $enabled = $settings['2fa_enable']; |
| 149: | } |
| 150: | $res = '<tr><td colspan="2" data-target=".tfa_setting" class="settings_subtitle cursor-pointer border-bottom p-2">'. |
| 151: | '<i class="bi bi-unlock-fill fs-5 me-2"></i>'.$this->trans('2 Factor Authentication').'</td></tr>'; |
| 152: | |
| 153: | $res .= '<tr class="tfa_setting"><td><label class="form-check-label">'.$this->trans('Enable 2 factor authentication').'</label>'. |
| 154: | '<input class="form-check-input ms-3" value="1" type="checkbox" name="2fa_enable"'; |
| 155: | if ($enabled) { |
| 156: | $res .= ' checked="checked"'; |
| 157: | } |
| 158: | $res .= '>'; |
| 159: | $svg = $this->get('2fa_svg'); |
| 160: | |
| 161: | if ($svg) { |
| 162: | $qr_code = ''; |
| 163: | if (!$enabled) { |
| 164: | $qr_code .= '<div class="err settings_wrap_text tfa_mt_1">'.$this->trans('Configure your authentication app using the barcode below BEFORE enabling 2 factor authentication.').'</div>'; |
| 165: | } |
| 166: | else { |
| 167: | $qr_code .= '<div>'.$this->trans('Update your settings with the code below').'</div>'; |
| 168: | } |
| 169: | |
| 170: | $qr_code .= $svg; |
| 171: | $qr_code .= '<div class="tfa_mb_1">'.$this->trans('If you can\'t use the QR code, you can enter the code below manually (no line breaks)').'</div>'; |
| 172: | $qr_code .= wordwrap($this->html_safe($this->get('2fa_secret', '')), 60, '<br />', true); |
| 173: | } |
| 174: | else { |
| 175: | $qr_code = '<div class="tfa_mt_1">'.$this->trans('Unable to generate 2 factor authentication QR code').'</div>'; |
| 176: | } |
| 177: | $res .= $qr_code; |
| 178: | |
| 179: | $res .= '<div class="tfa_mb_1">'.$this->trans('The following backup codes can be used to access your account if you lose your device'). '</div>'; |
| 180: | |
| 181: | foreach ($backup_codes as $val) { |
| 182: | $res .= ' '.$val.'<input type="hidden" name="2fa_backup_codes[]" value="'.$val.'" /></br >'; |
| 183: | } |
| 184: | $res .= '<div class="tfa_mt_1"> |
| 185: | <fieldset class="tfa_confirmation_fieldset p-3"> |
| 186: | <legend>Enter the confirmation code</legend> |
| 187: | <div class="tfa_confirmation_wrapper"> |
| 188: | <div class="tfa_confirmation_form"> |
| 189: | <div class="tfa_confirmation_input_digits"> |
| 190: | <input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 0" aria-required="true"> |
| 191: | <input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 1" aria-required="true"> |
| 192: | <input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 2" aria-required="true"> |
| 193: | <input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 3" aria-required="true"> |
| 194: | <input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 4" aria-required="true"> |
| 195: | <input class="tfa_confirmation_input_digit" type="number" aria-label="Digit 5" aria-required="true"> |
| 196: | </div> |
| 197: | <button id="tfaConfirmationBtn" type="submit" class="tfa_confirmation_input_button btn btn-light border-1">'.$this->trans('Verify code').'</button> |
| 198: | </div> |
| 199: | <div class="tfa_confirmation_hint"> '.$this->trans('Enter the 6 digit code from your Authenticator application').'</div> |
| 200: | </div> |
| 201: | </fieldset> |
| 202: | </div> |
| 203: | </td> |
| 204: | </tr>'; |
| 205: | return $res; |
| 206: | } |
| 207: | } |
| 208: | |
| 209: | |
| 210: | |
| 211: | |
| 212: | class Hm_Output_2fa_dialog extends Hm_Output_Module { |
| 213: | protected function output() { |
| 214: | |
| 215: | if ($this->get('2fa_required')) { |
| 216: | |
| 217: | $lang = 'en-us'; |
| 218: | $dir = 'ltr'; |
| 219: | if ($this->lang) { |
| 220: | $lang = mb_strtolower(str_replace('_', '-', $this->lang)); |
| 221: | } |
| 222: | if ($this->dir) { |
| 223: | $dir = $this->dir; |
| 224: | } |
| 225: | $class = $dir."_page"; |
| 226: | |
| 227: | if ($this->get('2fa_error')) { |
| 228: | $error = '<div class="tfa_error"><div class="alert alert-danger alert-dismissible fade show" role="alert">'.$this->trans($this->get('2fa_error')).'</div></div>'; |
| 229: | } |
| 230: | else { |
| 231: | $error = ''; |
| 232: | } |
| 233: | |
| 234: | if(!$this->get('fancy_login_allowed')){ |
| 235: | echo '<!DOCTYPE html> |
| 236: | <html lang="'.$this->html_safe($lang).'" class="'.$this->html_safe($class).'" dir="'.$this->html_safe($dir).'"> |
| 237: | <head> |
| 238: | <meta charset="utf-8" /> |
| 239: | <link href="site.css" media="all" rel="stylesheet" type="text/css" /> |
| 240: | <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"> |
| 241: | <link href="modules/themes/assets/default/css/default.css?v=' . CACHE_ID . '" media="all" rel="stylesheet" type="text/css" /> |
| 242: | <link href="vendor/twbs/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet" type="text/css" /> |
| 243: | </head> |
| 244: | <body> |
| 245: | <div class="bg-light"> |
| 246: | <div class="d-flex align-items-center justify-content-center vh-100 p-3"> |
| 247: | <div class="card col-12 col-md-6 col-lg-4 p-3"> |
| 248: | <div class="card-body"> |
| 249: | <form class="mt-5" method="POST"> |
| 250: | <p class="text-center"><img class="w-50" src="modules/core/assets/images/logo_dark.svg"></p> |
| 251: | <p class="text-center">'.$this->trans('Enter the 6 digit code from your Authenticator application').'</p> |
| 252: | '.$error.' |
| 253: | <div class="form-floating mb-3"> |
| 254: | <input autofocus required id="2fa_code" type="number" name="2fa_code" class="form-control" value="" placeholder="'.$this->trans('Login code').'"> |
| 255: | <label for="2fa_code">'.$this->trans('Login code').'</label> |
| 256: | </div> |
| 257: | <div class="d-grid"> |
| 258: | <input type="submit" class="btn btn-primary btn-lg" value="'.$this->trans('Submit').'"> |
| 259: | </div> |
| 260: | <input type="hidden" name="hm_page_key" value="'.$this->get('2fa_key').'"> |
| 261: | </form> |
| 262: | </div> |
| 263: | </div> |
| 264: | </div> |
| 265: | </div> |
| 266: | </body> |
| 267: | </html>'; |
| 268: | } |
| 269: | else{ |
| 270: | $style = '<style type="text/css">body,html{max-width:100vw !important; max-height:100vh !important; overflow:hidden !important;}.form-container{background-color:#f1f1f1;'. |
| 271: | 'background: linear-gradient( rgba(4, 26, 0, 0.85), rgba(4, 26, 0, 0.85)), url('.WEB_ROOT.'modules/core/assets/images/cloud.jpg);'. |
| 272: | 'background-attachment: fixed;background-position: center;background-repeat: no-repeat;background-size: cover;'. |
| 273: | 'display:grid; place-items:center; height:100vh; width:100vw;} .logged_out{display:block !important;}.sys_messages'. |
| 274: | '{position:fixed;right:20px;top:15px;min-height:30px;display:none;background-color:#fff;color:teal;'. |
| 275: | 'margin-top:0px;padding:15px;padding-bottom:5px;white-space:nowrap;border:solid 1px #999;border-radius:'. |
| 276: | '5px;filter:drop-shadow(4px 4px 4px #ccc);z-index:101;}.g-recaptcha{margin:0px 10px 10px 10px;}.mobile .g-recaptcha{'. |
| 277: | 'margin:0px 10px 5px 10px;}.title{font-weight:normal;padding:0px;margin:0px;margin-left:20px;'. |
| 278: | 'margin-bottom:20px;letter-spacing:-1px;color:#999;}html,body{min-width:100px !important;'. |
| 279: | 'background-color:#fff;}body{background:linear-gradient(180deg,#faf6f5,#faf6f5,#faf6f5,#faf6f5,'. |
| 280: | '#fff);font-size:1em;color:#333;font-family:Arial;padding:0px;margin:0px;min-width:700px;'. |
| 281: | 'font-size:100%;}input,option,select{font-size:100%;padding:3px;}textarea,select,input{border:outset '. |
| 282: | '1px #ddd;background-color:#fff;color:#333;border-radius:6px;}.screen_reader{position:absolute'. |
| 283: | ';top:auto;width:1px;height:1px;overflow:hidden;}.login_form{display:flex; justify-content:space-evenly; align-items:center; flex-direction:column;font-size:90%;'. |
| 284: | 'padding-top:60px;height:360px;border-radius:20px 20px 20px 20px;margin:0px;background-color:rgba(0,0,0,.6);'. |
| 285: | 'min-width:300px;}.login_form input{clear:both;float:left;padding:4px;'. |
| 286: | 'margin-top:10px;margin-bottom:10px;} .err{color:red !important;}.long_session'. |
| 287: | '{float:left;}.long_session input{padding:0px;float:none;font-size:18px;}.mobile .long_session{float:left;clear:both;} @media screen and (min-width:400px){.login_form{min-width:400px;}}'. |
| 288: | '.user-icon_signin{display:block; background-color:white; border-radius:100%; padding:10px; height:40px; margin-top:-75px; box-shadow: #6eb549 .4px 2.4px 6.2px; }'. |
| 289: | '.label_signin{width:210px; margin:0px 0px -18px 0px;color:#fff;opacity:0.7;}'. |
| 290: | '.login_form {float : none; padding-left : 0px; padding : 8px; }@media (max-height : 500px){ .user-icon_signin{display:none;}}'. |
| 291: | '.tfa_error{margin-left:0 !important; margin-right:0 !important; color:#f93838 !important;} .tfa_input{margin-left:0px;}'. |
| 292: | '</style>'; |
| 293: | echo '<!DOCTYPE html><html lang='.$this->html_safe($lang).' class="'.$this->html_safe($class). |
| 294: | '" dir="'.$this->html_safe($dir).'"><head><meta charset="utf-8" />'. |
| 295: | '<link href="site.css" media="all" rel="stylesheet" type="text/css" />'. |
| 296: | '<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">'.$style. |
| 297: | '</head><body><div class="form-container"> |
| 298: | <form class="login_form" method="POST"> |
| 299: | <svg class="user-icon_signin" viewBox="0 0 20 20"><path d="M12.075,10.812c1.358-0.853,2.242-2.507,2.242-4.037c0-2.181-1.795-4.618-4.198-4.618S5.921,4.594,5.921,6.775c0,1.53,0.884,3.185,2.242,4.037c-3.222,0.865-5.6,3.807-5.6,7.298c0,0.23,0.189,0.42,0.42,0.42h14.273c0.23,0,0.42-0.189,0.42-0.42C17.676,14.619,15.297,11.677,12.075,10.812 M6.761,6.775c0-2.162,1.773-3.778,3.358-3.778s3.359,1.616,3.359,3.778c0,2.162-1.774,3.778-3.359,3.778S6.761,8.937,6.761,6.775 M3.415,17.69c0.218-3.51,3.142-6.297,6.704-6.297c3.562,0,6.486,2.787,6.705,6.297H3.415z"></path></svg> |
| 300: | <img src="modules/core/assets/images/logo.svg" style="height:90px;"><!--h1 class="title">'. |
| 301: | $this->html_safe($this->get('router_app_name')).'</h1-->'. $error.'<div class="tfa_input">'. |
| 302: | '<label class="label_signin" for="2fa_code">'.$this->trans('Enter the 6 digit code from your Authenticator application'). |
| 303: | '</label></div><input type="hidden" name="hm_page_key" value="'.$this->get('2fa_key').'" />'. |
| 304: | '<input autofocus required id="2fa_code" style="width:200px; height:25px;" type="number" name="2fa_code" value="" placeholder="'. |
| 305: | $this->trans('Login code').'" /><input style="cursor:pointer; display:block; width: 210px; background-color:#6eb549; color:white; height:40px;" type="submit" value="'.$this->trans('Submit'). |
| 306: | '" /></form></div></body></html>'; |
| 307: | } |
| 308: | Hm_Functions::cease(); |
| 309: | } |
| 310: | } |
| 311: | } |
| 312: | |
| 313: | |
| 314: | |
| 315: | |
| 316: | if (!hm_exists('check_2fa_pin')) { |
| 317: | function check_2fa_pin($pin, $secret, $pass_len=6) { |
| 318: | $pin_mod = pow(10, $pass_len); |
| 319: | $time = floor(time()/30); |
| 320: | $time = pack('N', $time); |
| 321: | $time = str_pad($time, 8, chr(0), STR_PAD_LEFT); |
| 322: | $hash = hash_hmac('sha1', $time, $secret, true); |
| 323: | $offset = ord(substr($hash,-1)) & 0xF; |
| 324: | $input = substr($hash, $offset, strlen($hash) - $offset); |
| 325: | $input = unpack("N",substr($input, 0, 4)); |
| 326: | $inthash = $input[1] & 0x7FFFFFFF; |
| 327: | return $pin === str_pad($inthash % $pin_mod, 6, "0", STR_PAD_LEFT); |
| 328: | }} |
| 329: | |
| 330: | |
| 331: | |
| 332: | |
| 333: | if (!hm_exists('get_2fa_key')) { |
| 334: | function get_2fa_key($config) { |
| 335: | $secret = $config->get('2fa_secret', false); |
| 336: | $simple = $config->get('2fa_simple', false); |
| 337: | return array($secret, $simple); |
| 338: | }} |
| 339: | |
| 340: | |
| 341: | |
| 342: | |
| 343: | if (!hm_exists('base32_encode_str')) { |
| 344: | function base32_encode_str($str) { |
| 345: | return Base32\Base32::encode($str); |
| 346: | }} |
| 347: | |
| 348: | |
| 349: | |
| 350: | |
| 351: | if (!hm_exists('generate_qr_code')) { |
| 352: | function generate_qr_code($config, $username, $str) { |
| 353: | $renderer = new ImageRenderer( |
| 354: | new RendererStyle(200), |
| 355: | new SvgImageBackEnd() |
| 356: | ); |
| 357: | $writer = new Writer($renderer); |
| 358: | return $writer->writeString($str); |
| 359: | }} |
| 360: | |
| 361: | |
| 362: | |
| 363: | |
| 364: | if (!hm_exists('create_secret')) { |
| 365: | function create_secret($key, $user, $len) { |
| 366: | return Hm_Crypt::pbkdf2($key, $user, $len, 256, 'sha512'); |
| 367: | }} |
| 368: | |
| 369: | |
| 370: | |
| 371: | |
| 372: | if (!hm_exists('backup_codes')) { |
| 373: | function backup_codes($config) { |
| 374: | $codes = $config->get('2fa_backup_codes_setting', array()); |
| 375: | if (is_array($codes) && count($codes) == 3) { |
| 376: | return $codes; |
| 377: | } |
| 378: | return array(random_int(100000000, 999999999), random_int(100000000, 999999999), random_int(100000000, 999999999)); |
| 379: | }} |
| 380: | |