| 1 | efrain | 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/ParagonIE/ConstantTime/EncoderInterface.php');
 | 
        
           |  |  | 29 | require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/Binary.php');
 | 
        
           |  |  | 30 | require_once(__DIR__.'/../extlib/ParagonIE/ConstantTime/Base32.php');
 | 
        
           |  |  | 31 |   | 
        
           | 1441 | ariadna | 32 | use MoodleQuickForm;
 | 
        
           | 1 | efrain | 33 | use tool_mfa\local\factor\object_factor_base;
 | 
        
           |  |  | 34 | use OTPHP\TOTP;
 | 
        
           |  |  | 35 | use stdClass;
 | 
        
           | 1441 | ariadna | 36 | use core\clock;
 | 
        
           |  |  | 37 | use core\context\system;
 | 
        
           |  |  | 38 | use core\di;
 | 
        
           | 1 | efrain | 39 |   | 
        
           |  |  | 40 | /**
 | 
        
           |  |  | 41 |  * TOTP factor class.
 | 
        
           |  |  | 42 |  *
 | 
        
           |  |  | 43 |  * @package     factor_totp
 | 
        
           |  |  | 44 |  * @subpackage  tool_mfa
 | 
        
           |  |  | 45 |  * @author      Mikhail Golenkov <golenkovm@gmail.com>
 | 
        
           |  |  | 46 |  * @copyright   Catalyst IT
 | 
        
           |  |  | 47 |  * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 | 
        
           |  |  | 48 |  */
 | 
        
           |  |  | 49 | class factor extends object_factor_base {
 | 
        
           |  |  | 50 |   | 
        
           |  |  | 51 |     /** @var string */
 | 
        
           |  |  | 52 |     const TOTP_OLD = 'old';
 | 
        
           |  |  | 53 |   | 
        
           |  |  | 54 |     /** @var string */
 | 
        
           |  |  | 55 |     const TOTP_FUTURE = 'future';
 | 
        
           |  |  | 56 |   | 
        
           |  |  | 57 |     /** @var string */
 | 
        
           |  |  | 58 |     const TOTP_USED = 'used';
 | 
        
           |  |  | 59 |   | 
        
           |  |  | 60 |     /** @var string */
 | 
        
           |  |  | 61 |     const TOTP_VALID = 'valid';
 | 
        
           |  |  | 62 |   | 
        
           |  |  | 63 |     /** @var string */
 | 
        
           |  |  | 64 |     const TOTP_INVALID = 'invalid';
 | 
        
           |  |  | 65 |   | 
        
           |  |  | 66 |     /** @var string Factor icon */
 | 
        
           |  |  | 67 |     protected $icon = 'fa-mobile-screen';
 | 
        
           |  |  | 68 |   | 
        
           | 1441 | ariadna | 69 |     /** @var clock */
 | 
        
           |  |  | 70 |     private readonly clock $clock;
 | 
        
           | 1 | efrain | 71 |   | 
        
           |  |  | 72 |     /**
 | 
        
           | 1441 | ariadna | 73 |      * Constructor.
 | 
        
           |  |  | 74 |      *
 | 
        
           |  |  | 75 |      * @param string $name
 | 
        
           |  |  | 76 |      */
 | 
        
           |  |  | 77 |     public function __construct(string $name) {
 | 
        
           |  |  | 78 |         parent::__construct($name);
 | 
        
           |  |  | 79 |         $this->clock = di::get(clock::class);
 | 
        
           |  |  | 80 |     }
 | 
        
           |  |  | 81 |   | 
        
           |  |  | 82 |   | 
        
           |  |  | 83 |     /**
 | 
        
           | 1 | efrain | 84 |      * Generates TOTP URI for given secret key.
 | 
        
           |  |  | 85 |      * Uses site name, hostname and user name to make GA account look like:
 | 
        
           |  |  | 86 |      * "Sitename hostname (username)".
 | 
        
           |  |  | 87 |      *
 | 
        
           |  |  | 88 |      * @param string $secret
 | 
        
           |  |  | 89 |      * @return string
 | 
        
           |  |  | 90 |      */
 | 
        
           |  |  | 91 |     public function generate_totp_uri(string $secret): string {
 | 
        
           |  |  | 92 |         global $USER, $SITE, $CFG;
 | 
        
           |  |  | 93 |         $host = parse_url($CFG->wwwroot, PHP_URL_HOST);
 | 
        
           | 1441 | ariadna | 94 |         $sitename = str_replace(':', '', format_string($SITE->fullname, true, ['context' => system::instance()]));
 | 
        
           | 1 | efrain | 95 |         $issuer = $sitename.' '.$host;
 | 
        
           | 1441 | ariadna | 96 |         $totp = TOTP::create($secret, clock: $this->clock);
 | 
        
           | 1 | efrain | 97 |         $totp->setLabel($USER->username);
 | 
        
           |  |  | 98 |         $totp->setIssuer($issuer);
 | 
        
           |  |  | 99 |         return $totp->getProvisioningUri();
 | 
        
           |  |  | 100 |     }
 | 
        
           |  |  | 101 |   | 
        
           |  |  | 102 |     /**
 | 
        
           |  |  | 103 |      * Generates HTML sting with QR code for given secret key.
 | 
        
           |  |  | 104 |      *
 | 
        
           |  |  | 105 |      * @param string $secret
 | 
        
           |  |  | 106 |      * @return string
 | 
        
           |  |  | 107 |      */
 | 
        
           |  |  | 108 |     public function generate_qrcode(string $secret): string {
 | 
        
           |  |  | 109 |         $uri = $this->generate_totp_uri($secret);
 | 
        
           |  |  | 110 |         $qrcode = new \TCPDF2DBarcode($uri, 'QRCODE');
 | 
        
           |  |  | 111 |         $image = $qrcode->getBarcodePngData(7, 7);
 | 
        
           |  |  | 112 |         $html = \html_writer::img('data:image/png;base64,' . base64_encode($image), '', ['width' => '150px']);
 | 
        
           |  |  | 113 |         return $html;
 | 
        
           |  |  | 114 |     }
 | 
        
           |  |  | 115 |   | 
        
           |  |  | 116 |     /**
 | 
        
           |  |  | 117 |      * TOTP state
 | 
        
           |  |  | 118 |      *
 | 
        
           |  |  | 119 |      * {@inheritDoc}
 | 
        
           |  |  | 120 |      */
 | 
        
           |  |  | 121 |     public function get_state(): string {
 | 
        
           |  |  | 122 |         global $USER;
 | 
        
           |  |  | 123 |         $userfactors = $this->get_active_user_factors($USER);
 | 
        
           |  |  | 124 |   | 
        
           |  |  | 125 |         // If no codes are setup then we must be neutral not unknown.
 | 
        
           |  |  | 126 |         if (count($userfactors) == 0) {
 | 
        
           |  |  | 127 |             return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
 | 
        
           |  |  | 128 |         }
 | 
        
           |  |  | 129 |   | 
        
           |  |  | 130 |         return parent::get_state();
 | 
        
           |  |  | 131 |     }
 | 
        
           |  |  | 132 |   | 
        
           |  |  | 133 |     /**
 | 
        
           |  |  | 134 |      * TOTP Factor implementation.
 | 
        
           |  |  | 135 |      *
 | 
        
           | 1441 | ariadna | 136 |      * @param MoodleQuickForm $mform
 | 
        
           |  |  | 137 |      * @return MoodleQuickForm $mform
 | 
        
           | 1 | efrain | 138 |      */
 | 
        
           | 1441 | ariadna | 139 |     public function setup_factor_form_definition(MoodleQuickForm $mform): MoodleQuickForm {
 | 
        
           | 1 | efrain | 140 |         $secret = $this->generate_secret_code();
 | 
        
           |  |  | 141 |         $mform->addElement('hidden', 'secret', $secret);
 | 
        
           |  |  | 142 |         $mform->setType('secret', PARAM_ALPHANUM);
 | 
        
           |  |  | 143 |   | 
        
           |  |  | 144 |         return $mform;
 | 
        
           |  |  | 145 |     }
 | 
        
           |  |  | 146 |   | 
        
           |  |  | 147 |     /**
 | 
        
           |  |  | 148 |      * TOTP Factor implementation.
 | 
        
           |  |  | 149 |      *
 | 
        
           | 1441 | ariadna | 150 |      * @param MoodleQuickForm $mform
 | 
        
           |  |  | 151 |      * @return MoodleQuickForm $mform
 | 
        
           | 1 | efrain | 152 |      */
 | 
        
           | 1441 | ariadna | 153 |     public function setup_factor_form_definition_after_data(MoodleQuickForm $mform): MoodleQuickForm {
 | 
        
           | 1 | efrain | 154 |         global $OUTPUT, $SITE, $USER;
 | 
        
           |  |  | 155 |   | 
        
           |  |  | 156 |         // Array of elements to allow XSS.
 | 
        
           |  |  | 157 |         $xssallowedelements = [];
 | 
        
           |  |  | 158 |   | 
        
           |  |  | 159 |         $headingstring = $mform->elementExists('replaceid') ? 'replacefactor' : 'setupfactor';
 | 
        
           |  |  | 160 |         $mform->addElement('html', $OUTPUT->heading(get_string($headingstring, 'factor_totp'), 2));
 | 
        
           |  |  | 161 |   | 
        
           |  |  | 162 |         $html = \html_writer::tag('p', get_string('setupfactor:intro', 'factor_totp'));
 | 
        
           |  |  | 163 |         $mform->addElement('html', $html);
 | 
        
           |  |  | 164 |   | 
        
           |  |  | 165 |         // Device name.
 | 
        
           |  |  | 166 |         $html = \html_writer::tag('p', get_string('setupfactor:instructionsdevicename', 'factor_totp'), ['class' => 'bold']);
 | 
        
           |  |  | 167 |         $mform->addElement('html', $html);
 | 
        
           |  |  | 168 |   | 
        
           |  |  | 169 |         $mform->addElement('text', 'devicename', get_string('setupfactor:devicename', 'factor_totp'), [
 | 
        
           |  |  | 170 |             'placeholder' => get_string('devicenameexample', 'factor_totp'),
 | 
        
           |  |  | 171 |             'autofocus' => 'autofocus',
 | 
        
           |  |  | 172 |         ]);
 | 
        
           |  |  | 173 |         $mform->setType('devicename', PARAM_TEXT);
 | 
        
           |  |  | 174 |         $mform->addRule('devicename', get_string('required'), 'required', null, 'client');
 | 
        
           |  |  | 175 |   | 
        
           |  |  | 176 |         $html = \html_writer::tag('p', get_string('setupfactor:devicenameinfo', 'factor_totp'));
 | 
        
           |  |  | 177 |         $mform->addElement('static', 'devicenameinfo', '', $html);
 | 
        
           |  |  | 178 |   | 
        
           |  |  | 179 |         // Scan QR code.
 | 
        
           |  |  | 180 |         $html = \html_writer::tag('p', get_string('setupfactor:instructionsscan', 'factor_totp'), ['class' => 'bold']);
 | 
        
           |  |  | 181 |         $mform->addElement('html', $html);
 | 
        
           |  |  | 182 |   | 
        
           |  |  | 183 |         $secretfield = $mform->getElement('secret');
 | 
        
           |  |  | 184 |         $secret = $secretfield->getValue();
 | 
        
           |  |  | 185 |         $qrcode = $this->generate_qrcode($secret);
 | 
        
           |  |  | 186 |   | 
        
           |  |  | 187 |         $html = \html_writer::tag('p', $qrcode);
 | 
        
           |  |  | 188 |         $mform->addElement('static', 'scan', '', $html);
 | 
        
           |  |  | 189 |   | 
        
           |  |  | 190 |         // Enter manually.
 | 
        
           |  |  | 191 |         $secret = wordwrap($secret, 4, ' ', true) . '</code>';
 | 
        
           |  |  | 192 |         $secret = \html_writer::tag('code', $secret);
 | 
        
           |  |  | 193 |   | 
        
           | 1441 | ariadna | 194 |         $sitefullname = format_string($SITE->fullname, true, ['context' => system::instance()]);
 | 
        
           |  |  | 195 |   | 
        
           | 1 | efrain | 196 |         $manualtable = new \html_table();
 | 
        
           |  |  | 197 |         $manualtable->id = 'manualattributes';
 | 
        
           |  |  | 198 |         $manualtable->attributes['class'] = 'generaltable table table-bordered table-sm w-auto';
 | 
        
           |  |  | 199 |         $manualtable->attributes['style'] = 'width: auto;';
 | 
        
           |  |  | 200 |         $manualtable->data = [
 | 
        
           |  |  | 201 |             [get_string('setupfactor:key', 'factor_totp'), $secret],
 | 
        
           | 1441 | ariadna | 202 |             [get_string('setupfactor:account', 'factor_totp'), "{$sitefullname} ({$USER->username})"],
 | 
        
           | 1 | efrain | 203 |             [get_string('setupfactor:mode', 'factor_totp'), get_string('setupfactor:mode:timebased', 'factor_totp')],
 | 
        
           |  |  | 204 |         ];
 | 
        
           |  |  | 205 |   | 
        
           |  |  | 206 |         $html = \html_writer::table($manualtable);
 | 
        
           |  |  | 207 |         // Wrap the table in a couple of divs to be controlled via bootstrap.
 | 
        
           |  |  | 208 |         $html = \html_writer::div($html, 'collapse', ['id' => 'collapseManualAttributes']);
 | 
        
           |  |  | 209 |   | 
        
           |  |  | 210 |         $togglelink = \html_writer::tag('a', get_string('setupfactor:link', 'factor_totp'), [
 | 
        
           | 1441 | ariadna | 211 |             'data-bs-toggle' => 'collapse',
 | 
        
           |  |  | 212 |             'data-bs-target' => '#collapseManualAttributes',
 | 
        
           | 1 | efrain | 213 |             'aria-expanded' => 'false',
 | 
        
           |  |  | 214 |             'aria-controls' => 'collapseManualAttributes',
 | 
        
           |  |  | 215 |             'href' => '#',
 | 
        
           |  |  | 216 |         ]);
 | 
        
           |  |  | 217 |   | 
        
           |  |  | 218 |         $html = $togglelink . $html;
 | 
        
           |  |  | 219 |         $xssallowedelements[] = $mform->addElement('static', 'enter', '', $html);
 | 
        
           |  |  | 220 |   | 
        
           |  |  | 221 |         // Allow XSS.
 | 
        
           |  |  | 222 |         if (method_exists('MoodleQuickForm_static', 'set_allow_xss')) {
 | 
        
           |  |  | 223 |             foreach ($xssallowedelements as $xssallowedelement) {
 | 
        
           |  |  | 224 |                 $xssallowedelement->set_allow_xss(true);
 | 
        
           |  |  | 225 |             }
 | 
        
           |  |  | 226 |         }
 | 
        
           |  |  | 227 |   | 
        
           |  |  | 228 |         // Verification.
 | 
        
           |  |  | 229 |         $html = \html_writer::tag('p', get_string('setupfactor:instructionsverification', 'factor_totp'), ['class' => 'bold']);
 | 
        
           |  |  | 230 |         $mform->addElement('html', $html);
 | 
        
           |  |  | 231 |   | 
        
           |  |  | 232 |         $verificationfield = new \tool_mfa\local\form\verification_field(
 | 
        
           |  |  | 233 |             attributes: ['class' => 'tool-mfa-verification-code'],
 | 
        
           |  |  | 234 |             auth: false,
 | 
        
           |  |  | 235 |             elementlabel: get_string('setupfactor:verificationcode', 'factor_totp'),
 | 
        
           |  |  | 236 |         );
 | 
        
           |  |  | 237 |         $mform->addElement($verificationfield);
 | 
        
           |  |  | 238 |         $mform->setType('verificationcode', PARAM_ALPHANUM);
 | 
        
           |  |  | 239 |         $mform->addRule('verificationcode', get_string('required'), 'required', null, 'client');
 | 
        
           |  |  | 240 |   | 
        
           |  |  | 241 |         return $mform;
 | 
        
           |  |  | 242 |     }
 | 
        
           |  |  | 243 |   | 
        
           |  |  | 244 |     /**
 | 
        
           |  |  | 245 |      * TOTP Factor implementation.
 | 
        
           |  |  | 246 |      *
 | 
        
           |  |  | 247 |      * @param array $data
 | 
        
           |  |  | 248 |      * @return array
 | 
        
           |  |  | 249 |      */
 | 
        
           |  |  | 250 |     public function setup_factor_form_validation(array $data): array {
 | 
        
           |  |  | 251 |         $errors = [];
 | 
        
           |  |  | 252 |   | 
        
           | 1441 | ariadna | 253 |         $totp = TOTP::create($data['secret'], clock: $this->clock);
 | 
        
           |  |  | 254 |         if (!$totp->verify($data['verificationcode'], $this->clock->time(), 1)) {
 | 
        
           | 1 | efrain | 255 |             $errors['verificationcode'] = get_string('error:wrongverification', 'factor_totp');
 | 
        
           |  |  | 256 |         }
 | 
        
           |  |  | 257 |   | 
        
           |  |  | 258 |         return $errors;
 | 
        
           |  |  | 259 |     }
 | 
        
           |  |  | 260 |   | 
        
           |  |  | 261 |     /**
 | 
        
           |  |  | 262 |      * TOTP Factor implementation.
 | 
        
           |  |  | 263 |      *
 | 
        
           | 1441 | ariadna | 264 |      * @param MoodleQuickForm $mform
 | 
        
           |  |  | 265 |      * @return MoodleQuickForm $mform
 | 
        
           | 1 | efrain | 266 |      */
 | 
        
           | 1441 | ariadna | 267 |     public function login_form_definition(MoodleQuickForm $mform): MoodleQuickForm {
 | 
        
           | 1 | efrain | 268 |   | 
        
           |  |  | 269 |         $mform->disable_form_change_checker();
 | 
        
           |  |  | 270 |         $mform->addElement(new \tool_mfa\local\form\verification_field());
 | 
        
           |  |  | 271 |         $mform->setType('verificationcode', PARAM_ALPHANUM);
 | 
        
           |  |  | 272 |   | 
        
           |  |  | 273 |         return $mform;
 | 
        
           |  |  | 274 |     }
 | 
        
           |  |  | 275 |   | 
        
           |  |  | 276 |     /**
 | 
        
           |  |  | 277 |      * TOTP Factor implementation.
 | 
        
           |  |  | 278 |      *
 | 
        
           |  |  | 279 |      * @param array $data
 | 
        
           |  |  | 280 |      * @return array
 | 
        
           |  |  | 281 |      */
 | 
        
           |  |  | 282 |     public function login_form_validation(array $data): array {
 | 
        
           |  |  | 283 |         global $USER;
 | 
        
           |  |  | 284 |         $factors = $this->get_active_user_factors($USER);
 | 
        
           |  |  | 285 |         $result = ['verificationcode' => get_string('error:wrongverification', 'factor_totp')];
 | 
        
           | 1441 | ariadna | 286 |         $window = get_config('factor_totp', 'window');
 | 
        
           | 1 | efrain | 287 |   | 
        
           |  |  | 288 |         foreach ($factors as $factor) {
 | 
        
           | 1441 | ariadna | 289 |             $totp = TOTP::create($factor->secret, clock: $this->clock);
 | 
        
           | 1 | efrain | 290 |             $factorresult = $this->validate_code($data['verificationcode'], $window, $totp, $factor);
 | 
        
           | 1441 | ariadna | 291 |             $time = userdate($this->clock->time(), get_string('systimeformat', 'factor_totp'));
 | 
        
           | 1 | efrain | 292 |   | 
        
           |  |  | 293 |             switch ($factorresult) {
 | 
        
           |  |  | 294 |                 case self::TOTP_USED:
 | 
        
           |  |  | 295 |                     return ['verificationcode' => get_string('error:codealreadyused', 'factor_totp')];
 | 
        
           |  |  | 296 |   | 
        
           |  |  | 297 |                 case self::TOTP_OLD:
 | 
        
           |  |  | 298 |                     return ['verificationcode' => get_string('error:oldcode', 'factor_totp', $time)];
 | 
        
           |  |  | 299 |   | 
        
           |  |  | 300 |                 case self::TOTP_FUTURE:
 | 
        
           |  |  | 301 |                     return ['verificationcode' => get_string('error:futurecode', 'factor_totp', $time)];
 | 
        
           |  |  | 302 |   | 
        
           |  |  | 303 |                 case self::TOTP_VALID:
 | 
        
           |  |  | 304 |                     $this->update_lastverified($factor->id);
 | 
        
           |  |  | 305 |                     return [];
 | 
        
           |  |  | 306 |   | 
        
           |  |  | 307 |                 default:
 | 
        
           |  |  | 308 |                     continue(2);
 | 
        
           |  |  | 309 |             }
 | 
        
           |  |  | 310 |         }
 | 
        
           |  |  | 311 |         return $result;
 | 
        
           |  |  | 312 |     }
 | 
        
           |  |  | 313 |   | 
        
           |  |  | 314 |     /**
 | 
        
           |  |  | 315 |      * Checks the code for reuse, clock skew, and validity.
 | 
        
           |  |  | 316 |      *
 | 
        
           |  |  | 317 |      * @param string $code the code to check.
 | 
        
           |  |  | 318 |      * @param int $window the window to check validity for.
 | 
        
           |  |  | 319 |      * @param TOTP $totp the totp object to check against.
 | 
        
           |  |  | 320 |      * @param stdClass $factor the factor with information required.
 | 
        
           |  |  | 321 |      *
 | 
        
           |  |  | 322 |      * @return string constant with verification state.
 | 
        
           |  |  | 323 |      */
 | 
        
           |  |  | 324 |     public function validate_code(string $code, int $window, TOTP $totp, stdClass $factor): string {
 | 
        
           |  |  | 325 |         // First check if this code matches the last verified timestamp.
 | 
        
           |  |  | 326 |         $lastverified = $this->get_lastverified($factor->id);
 | 
        
           |  |  | 327 |         if ($lastverified > 0 && $totp->verify($code, $lastverified, $window)) {
 | 
        
           |  |  | 328 |             return self::TOTP_USED;
 | 
        
           |  |  | 329 |         }
 | 
        
           |  |  | 330 |   | 
        
           | 1441 | ariadna | 331 |         // Check if the code is valid, returning early.
 | 
        
           |  |  | 332 |         if ($totp->verify($code, $this->clock->time(), $window)) {
 | 
        
           | 1 | efrain | 333 |             return self::TOTP_VALID;
 | 
        
           |  |  | 334 |         }
 | 
        
           | 1441 | ariadna | 335 |   | 
        
           |  |  | 336 |         // Check for clock skew in the past and future 10 periods.
 | 
        
           |  |  | 337 |         for ($i = 1; $i <= 10; $i++) {
 | 
        
           |  |  | 338 |             $pasttimestamp = $this->clock->time() - $i * $totp->getPeriod();
 | 
        
           |  |  | 339 |             $futuretimestamp = $this->clock->time() + $i * $totp->getPeriod();
 | 
        
           |  |  | 340 |   | 
        
           |  |  | 341 |             if ($totp->verify($code, $pasttimestamp, $window)) {
 | 
        
           |  |  | 342 |                 return self::TOTP_OLD;
 | 
        
           |  |  | 343 |             }
 | 
        
           |  |  | 344 |   | 
        
           |  |  | 345 |             if ($totp->verify($code, $futuretimestamp, $window)) {
 | 
        
           |  |  | 346 |                 return self::TOTP_FUTURE;
 | 
        
           |  |  | 347 |             }
 | 
        
           |  |  | 348 |         }
 | 
        
           |  |  | 349 |   | 
        
           |  |  | 350 |         // In all other cases, the code is invalid.
 | 
        
           |  |  | 351 |         return self::TOTP_INVALID;
 | 
        
           | 1 | efrain | 352 |     }
 | 
        
           |  |  | 353 |   | 
        
           |  |  | 354 |     /**
 | 
        
           |  |  | 355 |      * Generates cryptographically secure pseudo-random 16-digit secret code.
 | 
        
           |  |  | 356 |      *
 | 
        
           |  |  | 357 |      * @return string
 | 
        
           |  |  | 358 |      */
 | 
        
           |  |  | 359 |     public function generate_secret_code(): string {
 | 
        
           | 1441 | ariadna | 360 |         $totp = TOTP::create(clock: $this->clock);
 | 
        
           | 1 | efrain | 361 |         return substr($totp->getSecret(), 0, 16);
 | 
        
           |  |  | 362 |     }
 | 
        
           |  |  | 363 |   | 
        
           |  |  | 364 |     /**
 | 
        
           |  |  | 365 |      * TOTP Factor implementation.
 | 
        
           |  |  | 366 |      *
 | 
        
           |  |  | 367 |      * @param stdClass $data
 | 
        
           |  |  | 368 |      * @return stdClass|null the factor record, or null.
 | 
        
           |  |  | 369 |      */
 | 
        
           |  |  | 370 |     public function setup_user_factor(stdClass $data): stdClass|null {
 | 
        
           |  |  | 371 |         global $DB, $USER;
 | 
        
           |  |  | 372 |   | 
        
           |  |  | 373 |         if (!empty($data->secret)) {
 | 
        
           |  |  | 374 |             $row = new stdClass();
 | 
        
           |  |  | 375 |             $row->userid = $USER->id;
 | 
        
           |  |  | 376 |             $row->factor = $this->name;
 | 
        
           |  |  | 377 |             $row->secret = $data->secret;
 | 
        
           |  |  | 378 |             $row->label = $data->devicename;
 | 
        
           | 1441 | ariadna | 379 |             $row->timecreated = $this->clock->time();
 | 
        
           | 1 | efrain | 380 |             $row->createdfromip = $USER->lastip;
 | 
        
           | 1441 | ariadna | 381 |             $row->timemodified = $this->clock->time();
 | 
        
           | 1 | efrain | 382 |             $row->lastverified = 0;
 | 
        
           |  |  | 383 |             $row->revoked = 0;
 | 
        
           |  |  | 384 |   | 
        
           |  |  | 385 |             // Check if a record with this configuration already exists, warning the user accordingly.
 | 
        
           |  |  | 386 |             $record = $DB->get_record('tool_mfa', [
 | 
        
           |  |  | 387 |                 'userid' => $row->userid,
 | 
        
           |  |  | 388 |                 'secret' => $row->secret,
 | 
        
           |  |  | 389 |                 'factor' => $row->factor,
 | 
        
           |  |  | 390 |             ], '*', IGNORE_MULTIPLE);
 | 
        
           |  |  | 391 |             if ($record) {
 | 
        
           |  |  | 392 |                 \core\notification::warning(get_string('error:alreadyregistered', 'factor_totp'));
 | 
        
           |  |  | 393 |                 return null;
 | 
        
           |  |  | 394 |             }
 | 
        
           |  |  | 395 |   | 
        
           |  |  | 396 |             $id = $DB->insert_record('tool_mfa', $row);
 | 
        
           |  |  | 397 |             $record = $DB->get_record('tool_mfa', ['id' => $id]);
 | 
        
           |  |  | 398 |             $this->create_event_after_factor_setup($USER);
 | 
        
           |  |  | 399 |   | 
        
           |  |  | 400 |             return $record;
 | 
        
           |  |  | 401 |         }
 | 
        
           |  |  | 402 |   | 
        
           |  |  | 403 |         return null;
 | 
        
           |  |  | 404 |     }
 | 
        
           |  |  | 405 |   | 
        
           |  |  | 406 |     /**
 | 
        
           |  |  | 407 |      * TOTP Factor implementation with replacement of existing factor.
 | 
        
           |  |  | 408 |      *
 | 
        
           |  |  | 409 |      * @param stdClass $data The new factor data.
 | 
        
           |  |  | 410 |      * @param int $id The id of the factor to replace.
 | 
        
           |  |  | 411 |      * @return stdClass|null the factor record, or null.
 | 
        
           |  |  | 412 |      */
 | 
        
           |  |  | 413 |     public function replace_user_factor(stdClass $data, int $id): stdClass|null {
 | 
        
           |  |  | 414 |         global $DB, $USER;
 | 
        
           |  |  | 415 |   | 
        
           |  |  | 416 |         $oldrecord = $DB->get_record('tool_mfa', ['id' => $id]);
 | 
        
           |  |  | 417 |         $newrecord = null;
 | 
        
           |  |  | 418 |   | 
        
           |  |  | 419 |         // Ensure we have a valid existing record before setting the new one.
 | 
        
           |  |  | 420 |         if ($oldrecord) {
 | 
        
           |  |  | 421 |             $newrecord = $this->setup_user_factor($data);
 | 
        
           |  |  | 422 |         }
 | 
        
           |  |  | 423 |         // Ensure the new record was created before revoking the old.
 | 
        
           |  |  | 424 |         if ($newrecord) {
 | 
        
           |  |  | 425 |             $this->revoke_user_factor($id);
 | 
        
           |  |  | 426 |         } else {
 | 
        
           |  |  | 427 |             \core\notification::warning(get_string('error:couldnotreplace', 'tool_mfa'));
 | 
        
           |  |  | 428 |             return null;
 | 
        
           |  |  | 429 |         }
 | 
        
           |  |  | 430 |         $this->create_event_after_factor_setup($USER);
 | 
        
           |  |  | 431 |   | 
        
           |  |  | 432 |         return $newrecord ?? null;
 | 
        
           |  |  | 433 |     }
 | 
        
           |  |  | 434 |   | 
        
           |  |  | 435 |     /**
 | 
        
           |  |  | 436 |      * TOTP Factor implementation.
 | 
        
           |  |  | 437 |      *
 | 
        
           |  |  | 438 |      * @param stdClass $user the user to check against.
 | 
        
           |  |  | 439 |      * @return array
 | 
        
           |  |  | 440 |      */
 | 
        
           |  |  | 441 |     public function get_all_user_factors($user): array {
 | 
        
           |  |  | 442 |         global $DB;
 | 
        
           |  |  | 443 |         return $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
 | 
        
           |  |  | 444 |     }
 | 
        
           |  |  | 445 |   | 
        
           |  |  | 446 |     /**
 | 
        
           |  |  | 447 |      * TOTP Factor implementation.
 | 
        
           |  |  | 448 |      *
 | 
        
           |  |  | 449 |      * {@inheritDoc}
 | 
        
           |  |  | 450 |      */
 | 
        
           |  |  | 451 |     public function has_revoke(): bool {
 | 
        
           |  |  | 452 |         return true;
 | 
        
           |  |  | 453 |     }
 | 
        
           |  |  | 454 |   | 
        
           |  |  | 455 |     /**
 | 
        
           |  |  | 456 |      * TOTP Factor implementation.
 | 
        
           |  |  | 457 |      */
 | 
        
           |  |  | 458 |     public function has_replace(): bool {
 | 
        
           |  |  | 459 |         return true;
 | 
        
           |  |  | 460 |     }
 | 
        
           |  |  | 461 |   | 
        
           |  |  | 462 |     /**
 | 
        
           |  |  | 463 |      * TOTP Factor implementation.
 | 
        
           |  |  | 464 |      *
 | 
        
           |  |  | 465 |      * {@inheritDoc}
 | 
        
           |  |  | 466 |      */
 | 
        
           |  |  | 467 |     public function has_setup(): bool {
 | 
        
           |  |  | 468 |         return true;
 | 
        
           |  |  | 469 |     }
 | 
        
           |  |  | 470 |   | 
        
           |  |  | 471 |     /**
 | 
        
           |  |  | 472 |      * TOTP Factor implementation
 | 
        
           |  |  | 473 |      *
 | 
        
           |  |  | 474 |      * {@inheritDoc}
 | 
        
           |  |  | 475 |      */
 | 
        
           |  |  | 476 |     public function show_setup_buttons(): bool {
 | 
        
           |  |  | 477 |         return true;
 | 
        
           |  |  | 478 |     }
 | 
        
           |  |  | 479 |   | 
        
           |  |  | 480 |     /**
 | 
        
           |  |  | 481 |      * TOTP Factor implementation.
 | 
        
           |  |  | 482 |      * Empty override of parent.
 | 
        
           |  |  | 483 |      *
 | 
        
           |  |  | 484 |      * {@inheritDoc}
 | 
        
           |  |  | 485 |      */
 | 
        
           |  |  | 486 |     public function post_pass_state(): void {
 | 
        
           |  |  | 487 |         return;
 | 
        
           |  |  | 488 |     }
 | 
        
           |  |  | 489 |   | 
        
           |  |  | 490 |     /**
 | 
        
           |  |  | 491 |      * TOTP Factor implementation.
 | 
        
           |  |  | 492 |      * TOTP cannot return fail state.
 | 
        
           |  |  | 493 |      *
 | 
        
           |  |  | 494 |      * @param stdClass $user
 | 
        
           |  |  | 495 |      */
 | 
        
           |  |  | 496 |     public function possible_states(stdClass $user): array {
 | 
        
           |  |  | 497 |         return [
 | 
        
           |  |  | 498 |             \tool_mfa\plugininfo\factor::STATE_PASS,
 | 
        
           |  |  | 499 |             \tool_mfa\plugininfo\factor::STATE_NEUTRAL,
 | 
        
           |  |  | 500 |             \tool_mfa\plugininfo\factor::STATE_UNKNOWN,
 | 
        
           |  |  | 501 |         ];
 | 
        
           |  |  | 502 |     }
 | 
        
           |  |  | 503 |   | 
        
           |  |  | 504 |     /**
 | 
        
           |  |  | 505 |      * TOTP Factor implementation.
 | 
        
           |  |  | 506 |      *
 | 
        
           |  |  | 507 |      * {@inheritDoc}
 | 
        
           |  |  | 508 |      */
 | 
        
           |  |  | 509 |     public function get_setup_string(): string {
 | 
        
           |  |  | 510 |         return get_string('setupfactorbutton', 'factor_totp');
 | 
        
           |  |  | 511 |     }
 | 
        
           |  |  | 512 |   | 
        
           |  |  | 513 |     /**
 | 
        
           |  |  | 514 |      * Gets the string for manage button on preferences page.
 | 
        
           |  |  | 515 |      *
 | 
        
           |  |  | 516 |      * @return string
 | 
        
           |  |  | 517 |      */
 | 
        
           |  |  | 518 |     public function get_manage_string(): string {
 | 
        
           |  |  | 519 |         return get_string('managefactorbutton', 'factor_totp');
 | 
        
           |  |  | 520 |     }
 | 
        
           |  |  | 521 | }
 |