1 <?php 2 // This file is part of Moodle - http://moodle.org/ 3 // 4 // Moodle is free software: you can redistribute it and/or modify 5 // it under the terms of the GNU General Public License as published by 6 // the Free Software Foundation, either version 3 of the License, or 7 // (at your option) any later version. 8 // 9 // Moodle is distributed in the hope that it will be useful, 10 // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 // GNU General Public License for more details. 13 // 14 // You should have received a copy of the GNU General Public License 15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 16 17 namespace factor_totp; 18 19 defined('MOODLE_INTERNAL') || die(); 20 21 require_once($CFG->libdir.'/tcpdf/tcpdf_barcodes_2d.php'); 22 require_once (__DIR__.'/../extlib/OTPHP/OTPInterface.php'); 23 require_once (__DIR__.'/../extlib/OTPHP/TOTPInterface.php'); 24 require_once (__DIR__.'/../extlib/OTPHP/ParameterTrait.php'); 25 require_once (__DIR__.'/../extlib/OTPHP/OTP.php'); 26 require_once (__DIR__.'/../extlib/OTPHP/TOTP.php'); 27 28 require_once (__DIR__.'/../extlib/Assert/Assertion.php'); 29 require_once (__DIR__.'/../extlib/Assert/AssertionFailedException.php'); 30 require_once (__DIR__.'/../extlib/Assert/InvalidArgumentException.php'); 31 require_once (__DIR__.'/../extlib/ParagonIE/ConstantTime/EncoderInterface.php'); 32 require_once (__DIR__.'/../extlib/ParagonIE/ConstantTime/Binary.php'); 33 require_once (__DIR__.'/../extlib/ParagonIE/ConstantTime/Base32.php'); 34 35 use tool_mfa\local\factor\object_factor_base; 36 use OTPHP\TOTP; 37 use stdClass; 38 39 /** 40 * TOTP factor class. 41 * 42 * @package factor_totp 43 * @subpackage tool_mfa 44 * @author Mikhail Golenkov <golenkovm@gmail.com> 45 * @copyright Catalyst IT 46 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 47 */ 48 class factor extends object_factor_base { 49 50 /** @var string */ 51 const TOTP_OLD = 'old'; 52 53 /** @var string */ 54 const TOTP_FUTURE = 'future'; 55 56 /** @var string */ 57 const TOTP_USED = 'used'; 58 59 /** @var string */ 60 const TOTP_VALID = 'valid'; 61 62 /** @var string */ 63 const TOTP_INVALID = 'invalid'; 64 65 /** @var string Factor icon */ 66 protected $icon = 'fa-mobile-screen'; 67 68 69 /** 70 * Generates TOTP URI for given secret key. 71 * Uses site name, hostname and user name to make GA account look like: 72 * "Sitename hostname (username)". 73 * 74 * @param string $secret 75 * @return string 76 */ 77 public function generate_totp_uri(string $secret): string { 78 global $USER, $SITE, $CFG; 79 $host = parse_url($CFG->wwwroot, PHP_URL_HOST); 80 $sitename = str_replace(':', '', $SITE->fullname); 81 $issuer = $sitename.' '.$host; 82 $totp = TOTP::create($secret); 83 $totp->setLabel($USER->username); 84 $totp->setIssuer($issuer); 85 return $totp->getProvisioningUri(); 86 } 87 88 /** 89 * Generates HTML sting with QR code for given secret key. 90 * 91 * @param string $secret 92 * @return string 93 */ 94 public function generate_qrcode(string $secret): string { 95 $uri = $this->generate_totp_uri($secret); 96 $qrcode = new \TCPDF2DBarcode($uri, 'QRCODE'); 97 $image = $qrcode->getBarcodePngData(7, 7); 98 $html = \html_writer::tag('p', get_string('setupfactor:scanwithapp', 'factor_totp')); 99 $html .= \html_writer::img('data:image/png;base64,' . base64_encode($image), '', ['width' => '150px']); 100 return $html; 101 } 102 103 /** 104 * TOTP state 105 * 106 * {@inheritDoc} 107 */ 108 public function get_state(): string { 109 global $USER; 110 $userfactors = $this->get_active_user_factors($USER); 111 112 // If no codes are setup then we must be neutral not unknown. 113 if (count($userfactors) == 0) { 114 return \tool_mfa\plugininfo\factor::STATE_NEUTRAL; 115 } 116 117 return parent::get_state(); 118 } 119 120 /** 121 * TOTP Factor implementation. 122 * 123 * @param \MoodleQuickForm $mform 124 * @return \MoodleQuickForm $mform 125 */ 126 public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm { 127 $secret = $this->generate_secret_code(); 128 $mform->addElement('hidden', 'secret', $secret); 129 $mform->setType('secret', PARAM_ALPHANUM); 130 131 return $mform; 132 } 133 134 /** 135 * TOTP Factor implementation. 136 * 137 * @param \MoodleQuickForm $mform 138 * @return \MoodleQuickForm $mform 139 */ 140 public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm { 141 global $OUTPUT, $SITE, $USER; 142 143 // Array of elements to allow XSS. 144 $xssallowedelements = []; 145 146 $mform->addElement('html', $OUTPUT->heading(get_string('setupfactor', 'factor_totp'), 2)); 147 $mform->addElement('html', \html_writer::tag('p', get_string('info', 'factor_totp'))); 148 $mform->addElement('html', \html_writer::tag('hr', '')); 149 150 $mform->addElement('text', 'devicename', get_string('devicename', 'factor_totp'), [ 151 'placeholder' => get_string('devicenameexample', 'factor_totp'), 152 'autofocus' => 'autofocus', 153 ]); 154 $mform->addHelpButton('devicename', 'devicename', 'factor_totp'); 155 $mform->setType('devicename', PARAM_TEXT); 156 $mform->addRule('devicename', get_string('required'), 'required', null, 'client'); 157 158 // Scan. 159 $secretfield = $mform->getElement('secret'); 160 $secret = $secretfield->getValue(); 161 $qrcode = $this->generate_qrcode($secret); 162 163 $html = \html_writer::tag('p', $qrcode); 164 $xssallowedelements[] = $mform->addElement('static', 'scan', get_string('setupfactor:scan', 'factor_totp'), $html); 165 166 // Link. 167 if (get_config('factor_totp', 'totplink')) { 168 $uri = $this->generate_totp_uri($secret); 169 $html = $OUTPUT->action_link($uri, get_string('setupfactor:linklabel', 'factor_totp')); 170 $xssallowedelements[] = $mform->addElement('static', 'link', get_string('setupfactor:link', 'factor_totp'), $html); 171 $mform->addHelpButton('link', 'setupfactor:link', 'factor_totp'); 172 } 173 174 // Enter manually. 175 $secret = wordwrap($secret, 4, ' ', true) . '</code>'; 176 $secret = \html_writer::tag('code', $secret); 177 178 $manualtable = new \html_table(); 179 $manualtable->id = 'manualattributes'; 180 $manualtable->attributes['class'] = 'generaltable table table-bordered table-sm w-auto'; 181 $manualtable->attributes['style'] = 'width: auto;'; 182 $manualtable->data = [ 183 [get_string('setupfactor:key', 'factor_totp'), $secret], 184 [get_string('setupfactor:account', 'factor_totp'), "$SITE->fullname ($USER->username)"], 185 [get_string('setupfactor:mode', 'factor_totp'), get_string('setupfactor:mode:timebased', 'factor_totp')], 186 ]; 187 188 $html = \html_writer::table($manualtable); 189 $html = \html_writer::tag('p', get_string('setupfactor:enter', 'factor_totp')) . $html; 190 // Wrap the table in a couple of divs to be controlled via bootstrap. 191 $html = \html_writer::div($html, 'card card-body', ['style' => 'padding-left: 0 !important;']); 192 $html = \html_writer::div($html, 'collapse', ['id' => 'collapseManualAttributes']); 193 194 $togglelink = \html_writer::tag('btn', get_string('setupfactor:scanfail', 'factor_totp'), [ 195 'class' => 'btn btn-secondary', 196 'type' => 'button', 197 'data-toggle' => 'collapse', 198 'data-target' => '#collapseManualAttributes', 199 'aria-expanded' => 'false', 200 'aria-controls' => 'collapseManualAttributes', 201 'style' => 'font-size: 14px;', 202 ]); 203 204 $html = $togglelink . $html; 205 $xssallowedelements[] = $mform->addElement('static', 'enter', '', $html); 206 207 // Allow XSS. 208 if (method_exists('MoodleQuickForm_static', 'set_allow_xss')) { 209 foreach ($xssallowedelements as $xssallowedelement) { 210 $xssallowedelement->set_allow_xss(true); 211 } 212 } 213 214 $mform->addElement(new \tool_mfa\local\form\verification_field(null, false)); 215 $mform->setType('verificationcode', PARAM_ALPHANUM); 216 $mform->addHelpButton('verificationcode', 'verificationcode', 'factor_totp'); 217 $mform->addRule('verificationcode', get_string('required'), 'required', null, 'client'); 218 219 return $mform; 220 } 221 222 /** 223 * TOTP Factor implementation. 224 * 225 * @param array $data 226 * @return array 227 */ 228 public function setup_factor_form_validation(array $data): array { 229 $errors = []; 230 231 $totp = TOTP::create($data['secret']); 232 if (!$totp->verify($data['verificationcode'], time(), 1)) { 233 $errors['verificationcode'] = get_string('error:wrongverification', 'factor_totp'); 234 } 235 236 return $errors; 237 } 238 239 /** 240 * TOTP Factor implementation. 241 * 242 * @param \MoodleQuickForm $mform 243 * @return \MoodleQuickForm $mform 244 */ 245 public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm { 246 247 $mform->disable_form_change_checker(); 248 $mform->addElement(new \tool_mfa\local\form\verification_field()); 249 $mform->setType('verificationcode', PARAM_ALPHANUM); 250 251 return $mform; 252 } 253 254 /** 255 * TOTP Factor implementation. 256 * 257 * @param array $data 258 * @return array 259 */ 260 public function login_form_validation(array $data): array { 261 global $USER; 262 $factors = $this->get_active_user_factors($USER); 263 $result = ['verificationcode' => get_string('error:wrongverification', 'factor_totp')]; 264 $windowconfig = get_config('factor_totp', 'window'); 265 266 foreach ($factors as $factor) { 267 $totp = TOTP::create($factor->secret); 268 // Convert seconds to windows. 269 $window = (int) floor($windowconfig / $totp->getPeriod()); 270 $factorresult = $this->validate_code($data['verificationcode'], $window, $totp, $factor); 271 $time = userdate(time(), get_string('systimeformat', 'factor_totp')); 272 273 switch ($factorresult) { 274 case self::TOTP_USED: 275 return ['verificationcode' => get_string('error:codealreadyused', 'factor_totp')]; 276 277 case self::TOTP_OLD: 278 return ['verificationcode' => get_string('error:oldcode', 'factor_totp', $time)]; 279 280 case self::TOTP_FUTURE: 281 return ['verificationcode' => get_string('error:futurecode', 'factor_totp', $time)]; 282 283 case self::TOTP_VALID: 284 $this->update_lastverified($factor->id); 285 return []; 286 287 default: 288 continue(2); 289 } 290 } 291 return $result; 292 } 293 294 /** 295 * Checks the code for reuse, clock skew, and validity. 296 * 297 * @param string $code the code to check. 298 * @param int $window the window to check validity for. 299 * @param TOTP $totp the totp object to check against. 300 * @param stdClass $factor the factor with information required. 301 * 302 * @return string constant with verification state. 303 */ 304 public function validate_code(string $code, int $window, TOTP $totp, stdClass $factor): string { 305 // First check if this code matches the last verified timestamp. 306 $lastverified = $this->get_lastverified($factor->id); 307 if ($lastverified > 0 && $totp->verify($code, $lastverified, $window)) { 308 return self::TOTP_USED; 309 } 310 311 // The window in which to check for clock skew, 5 increments past valid window. 312 $skewwindow = $window + 5; 313 $pasttimestamp = time() - ($skewwindow * $totp->getPeriod()); 314 $futuretimestamp = time() + ($skewwindow * $totp->getPeriod()); 315 316 if ($totp->verify($code, time(), $window)) { 317 return self::TOTP_VALID; 318 } else if ($totp->verify($code, $pasttimestamp, $skewwindow)) { 319 // Check for clock skew in the past 10 periods. 320 return self::TOTP_OLD; 321 } else if ($totp->verify($code, $futuretimestamp, $skewwindow)) { 322 // Check for clock skew in the future 10 periods. 323 return self::TOTP_FUTURE; 324 } else { 325 // In all other cases, code is invalid. 326 return self::TOTP_INVALID; 327 } 328 } 329 330 /** 331 * Generates cryptographically secure pseudo-random 16-digit secret code. 332 * 333 * @return string 334 */ 335 public function generate_secret_code(): string { 336 $totp = TOTP::create(); 337 return substr($totp->getSecret(), 0, 16); 338 } 339 340 /** 341 * TOTP Factor implementation. 342 * 343 * @param stdClass $data 344 * @return stdClass the factor record, or null. 345 */ 346 public function setup_user_factor(stdClass $data): stdClass|null { 347 global $DB, $USER; 348 349 if (!empty($data->secret)) { 350 $row = new stdClass(); 351 $row->userid = $USER->id; 352 $row->factor = $this->name; 353 $row->secret = $data->secret; 354 $row->label = $data->devicename; 355 $row->timecreated = time(); 356 $row->createdfromip = $USER->lastip; 357 $row->timemodified = time(); 358 $row->lastverified = 0; 359 $row->revoked = 0; 360 361 // Check if a record with this configuration already exists, warning the user accordingly. 362 $record = $DB->get_record('tool_mfa', [ 363 'userid' => $row->userid, 364 'secret' => $row->secret, 365 'factor' => $row->factor, 366 ], '*', IGNORE_MULTIPLE); 367 if ($record) { 368 \core\notification::warning(get_string('error:alreadyregistered', 'factor_totp')); 369 return $record; 370 } 371 372 $id = $DB->insert_record('tool_mfa', $row); 373 $record = $DB->get_record('tool_mfa', ['id' => $id]); 374 $this->create_event_after_factor_setup($USER); 375 376 return $record; 377 } 378 379 return null; 380 } 381 382 /** 383 * TOTP Factor implementation. 384 * 385 * @param stdClass $user the user to check against. 386 * @return array 387 */ 388 public function get_all_user_factors($user): array { 389 global $DB; 390 return $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]); 391 } 392 393 /** 394 * TOTP Factor implementation. 395 * 396 * {@inheritDoc} 397 */ 398 public function has_revoke(): bool { 399 return true; 400 } 401 402 /** 403 * TOTP Factor implementation. 404 * 405 * {@inheritDoc} 406 */ 407 public function has_setup(): bool { 408 return true; 409 } 410 411 /** 412 * TOTP Factor implementation 413 * 414 * {@inheritDoc} 415 */ 416 public function show_setup_buttons(): bool { 417 return true; 418 } 419 420 /** 421 * TOTP Factor implementation. 422 * Empty override of parent. 423 * 424 * {@inheritDoc} 425 */ 426 public function post_pass_state(): void { 427 return; 428 } 429 430 /** 431 * TOTP Factor implementation. 432 * TOTP cannot return fail state. 433 * 434 * @param stdClass $user 435 */ 436 public function possible_states(stdClass $user): array { 437 return [ 438 \tool_mfa\plugininfo\factor::STATE_PASS, 439 \tool_mfa\plugininfo\factor::STATE_NEUTRAL, 440 \tool_mfa\plugininfo\factor::STATE_UNKNOWN, 441 ]; 442 } 443 444 /** 445 * TOTP Factor implementation. 446 * 447 * {@inheritDoc} 448 */ 449 public function get_setup_string(): string { 450 return get_string('factorsetup', 'factor_totp'); 451 } 452 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body