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: * 2FA modules
9: * @package modules
10: * @subpackage 2fa
11: */
12:
13: if (!defined('DEBUG_MODE')) { die(); }
14:
15: /**
16: * @subpackage 2fa/handler
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: * @subpackage 2fa/handler
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: /*if(!extension_loaded('imagick')){
58: Hm_Msgs::add('2FA The imagick extension is required to use 2fa feature, please contact your administrator for fixing this', 'warning');
59: return;
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: * Verify 2fa code is paired with Authenticator app before enabling 2fa
114: * @subpackage 2fa/handler
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: * @subpackage 2fa/output
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: * @subpackage 2fa/output
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: * @subpackage 2fa/functions
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: * @subpackage 2fa/functions
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: * @subpackage 2fa/functions
342: */
343: if (!hm_exists('base32_encode_str')) {
344: function base32_encode_str($str) {
345: return Base32\Base32::encode($str);
346: }}
347:
348: /**
349: * @subpackage 2fa/functions
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: * @subpackage 2fa/functions
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: * @subpackage 2fa/functions
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: