Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
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/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::img('data:image/png;base64,' . base64_encode($image), '', ['width' => '150px']);
99
        return $html;
100
    }
101
 
102
    /**
103
     * TOTP state
104
     *
105
     * {@inheritDoc}
106
     */
107
    public function get_state(): string {
108
        global $USER;
109
        $userfactors = $this->get_active_user_factors($USER);
110
 
111
        // If no codes are setup then we must be neutral not unknown.
112
        if (count($userfactors) == 0) {
113
            return \tool_mfa\plugininfo\factor::STATE_NEUTRAL;
114
        }
115
 
116
        return parent::get_state();
117
    }
118
 
119
    /**
120
     * TOTP Factor implementation.
121
     *
122
     * @param \MoodleQuickForm $mform
123
     * @return \MoodleQuickForm $mform
124
     */
125
    public function setup_factor_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
126
        $secret = $this->generate_secret_code();
127
        $mform->addElement('hidden', 'secret', $secret);
128
        $mform->setType('secret', PARAM_ALPHANUM);
129
 
130
        return $mform;
131
    }
132
 
133
    /**
134
     * TOTP Factor implementation.
135
     *
136
     * @param \MoodleQuickForm $mform
137
     * @return \MoodleQuickForm $mform
138
     */
139
    public function setup_factor_form_definition_after_data(\MoodleQuickForm $mform): \MoodleQuickForm {
140
        global $OUTPUT, $SITE, $USER;
141
 
142
        // Array of elements to allow XSS.
143
        $xssallowedelements = [];
144
 
145
        $headingstring = $mform->elementExists('replaceid') ? 'replacefactor' : 'setupfactor';
146
        $mform->addElement('html', $OUTPUT->heading(get_string($headingstring, 'factor_totp'), 2));
147
 
148
        $html = \html_writer::tag('p', get_string('setupfactor:intro', 'factor_totp'));
149
        $mform->addElement('html', $html);
150
 
151
        // Device name.
152
        $html = \html_writer::tag('p', get_string('setupfactor:instructionsdevicename', 'factor_totp'), ['class' => 'bold']);
153
        $mform->addElement('html', $html);
154
 
155
        $mform->addElement('text', 'devicename', get_string('setupfactor:devicename', 'factor_totp'), [
156
            'placeholder' => get_string('devicenameexample', 'factor_totp'),
157
            'autofocus' => 'autofocus',
158
        ]);
159
        $mform->setType('devicename', PARAM_TEXT);
160
        $mform->addRule('devicename', get_string('required'), 'required', null, 'client');
161
 
162
        $html = \html_writer::tag('p', get_string('setupfactor:devicenameinfo', 'factor_totp'));
163
        $mform->addElement('static', 'devicenameinfo', '', $html);
164
 
165
        // Scan QR code.
166
        $html = \html_writer::tag('p', get_string('setupfactor:instructionsscan', 'factor_totp'), ['class' => 'bold']);
167
        $mform->addElement('html', $html);
168
 
169
        $secretfield = $mform->getElement('secret');
170
        $secret = $secretfield->getValue();
171
        $qrcode = $this->generate_qrcode($secret);
172
 
173
        $html = \html_writer::tag('p', $qrcode);
174
        $mform->addElement('static', 'scan', '', $html);
175
 
176
        // Enter manually.
177
        $secret = wordwrap($secret, 4, ' ', true) . '</code>';
178
        $secret = \html_writer::tag('code', $secret);
179
 
180
        $manualtable = new \html_table();
181
        $manualtable->id = 'manualattributes';
182
        $manualtable->attributes['class'] = 'generaltable table table-bordered table-sm w-auto';
183
        $manualtable->attributes['style'] = 'width: auto;';
184
        $manualtable->data = [
185
            [get_string('setupfactor:key', 'factor_totp'), $secret],
186
            [get_string('setupfactor:account', 'factor_totp'), "$SITE->fullname ($USER->username)"],
187
            [get_string('setupfactor:mode', 'factor_totp'), get_string('setupfactor:mode:timebased', 'factor_totp')],
188
        ];
189
 
190
        $html = \html_writer::table($manualtable);
191
        // Wrap the table in a couple of divs to be controlled via bootstrap.
192
        $html = \html_writer::div($html, 'collapse', ['id' => 'collapseManualAttributes']);
193
 
194
        $togglelink = \html_writer::tag('a', get_string('setupfactor:link', 'factor_totp'), [
195
            'data-toggle' => 'collapse',
196
            'data-target' => '#collapseManualAttributes',
197
            'aria-expanded' => 'false',
198
            'aria-controls' => 'collapseManualAttributes',
199
            'href' => '#',
200
        ]);
201
 
202
        $html = $togglelink . $html;
203
        $xssallowedelements[] = $mform->addElement('static', 'enter', '', $html);
204
 
205
        // Allow XSS.
206
        if (method_exists('MoodleQuickForm_static', 'set_allow_xss')) {
207
            foreach ($xssallowedelements as $xssallowedelement) {
208
                $xssallowedelement->set_allow_xss(true);
209
            }
210
        }
211
 
212
        // Verification.
213
        $html = \html_writer::tag('p', get_string('setupfactor:instructionsverification', 'factor_totp'), ['class' => 'bold']);
214
        $mform->addElement('html', $html);
215
 
216
        $verificationfield = new \tool_mfa\local\form\verification_field(
217
            attributes: ['class' => 'tool-mfa-verification-code'],
218
            auth: false,
219
            elementlabel: get_string('setupfactor:verificationcode', 'factor_totp'),
220
        );
221
        $mform->addElement($verificationfield);
222
        $mform->setType('verificationcode', PARAM_ALPHANUM);
223
        $mform->addRule('verificationcode', get_string('required'), 'required', null, 'client');
224
 
225
        return $mform;
226
    }
227
 
228
    /**
229
     * TOTP Factor implementation.
230
     *
231
     * @param array $data
232
     * @return array
233
     */
234
    public function setup_factor_form_validation(array $data): array {
235
        $errors = [];
236
 
237
        $totp = TOTP::create($data['secret']);
238
        if (!$totp->verify($data['verificationcode'], time(), 1)) {
239
            $errors['verificationcode'] = get_string('error:wrongverification', 'factor_totp');
240
        }
241
 
242
        return $errors;
243
    }
244
 
245
    /**
246
     * TOTP Factor implementation.
247
     *
248
     * @param \MoodleQuickForm $mform
249
     * @return \MoodleQuickForm $mform
250
     */
251
    public function login_form_definition(\MoodleQuickForm $mform): \MoodleQuickForm {
252
 
253
        $mform->disable_form_change_checker();
254
        $mform->addElement(new \tool_mfa\local\form\verification_field());
255
        $mform->setType('verificationcode', PARAM_ALPHANUM);
256
 
257
        return $mform;
258
    }
259
 
260
    /**
261
     * TOTP Factor implementation.
262
     *
263
     * @param array $data
264
     * @return array
265
     */
266
    public function login_form_validation(array $data): array {
267
        global $USER;
268
        $factors = $this->get_active_user_factors($USER);
269
        $result = ['verificationcode' => get_string('error:wrongverification', 'factor_totp')];
270
        $windowconfig = get_config('factor_totp', 'window');
271
 
272
        foreach ($factors as $factor) {
273
            $totp = TOTP::create($factor->secret);
274
            // Convert seconds to windows.
275
            $window = (int) floor($windowconfig / $totp->getPeriod());
276
            $factorresult = $this->validate_code($data['verificationcode'], $window, $totp, $factor);
277
            $time = userdate(time(), get_string('systimeformat', 'factor_totp'));
278
 
279
            switch ($factorresult) {
280
                case self::TOTP_USED:
281
                    return ['verificationcode' => get_string('error:codealreadyused', 'factor_totp')];
282
 
283
                case self::TOTP_OLD:
284
                    return ['verificationcode' => get_string('error:oldcode', 'factor_totp', $time)];
285
 
286
                case self::TOTP_FUTURE:
287
                    return ['verificationcode' => get_string('error:futurecode', 'factor_totp', $time)];
288
 
289
                case self::TOTP_VALID:
290
                    $this->update_lastverified($factor->id);
291
                    return [];
292
 
293
                default:
294
                    continue(2);
295
            }
296
        }
297
        return $result;
298
    }
299
 
300
    /**
301
     * Checks the code for reuse, clock skew, and validity.
302
     *
303
     * @param string $code the code to check.
304
     * @param int $window the window to check validity for.
305
     * @param TOTP $totp the totp object to check against.
306
     * @param stdClass $factor the factor with information required.
307
     *
308
     * @return string constant with verification state.
309
     */
310
    public function validate_code(string $code, int $window, TOTP $totp, stdClass $factor): string {
311
        // First check if this code matches the last verified timestamp.
312
        $lastverified = $this->get_lastverified($factor->id);
313
        if ($lastverified > 0 && $totp->verify($code, $lastverified, $window)) {
314
            return self::TOTP_USED;
315
        }
316
 
317
        // The window in which to check for clock skew, 5 increments past valid window.
318
        $skewwindow = $window + 5;
319
        $pasttimestamp = time() - ($skewwindow * $totp->getPeriod());
320
        $futuretimestamp = time() + ($skewwindow * $totp->getPeriod());
321
 
322
        if ($totp->verify($code, time(), $window)) {
323
            return self::TOTP_VALID;
324
        } else if ($totp->verify($code, $pasttimestamp, $skewwindow)) {
325
            // Check for clock skew in the past 10 periods.
326
            return self::TOTP_OLD;
327
        } else if ($totp->verify($code, $futuretimestamp, $skewwindow)) {
328
            // Check for clock skew in the future 10 periods.
329
            return self::TOTP_FUTURE;
330
        } else {
331
            // In all other cases, code is invalid.
332
            return self::TOTP_INVALID;
333
        }
334
    }
335
 
336
    /**
337
     * Generates cryptographically secure pseudo-random 16-digit secret code.
338
     *
339
     * @return string
340
     */
341
    public function generate_secret_code(): string {
342
        $totp = TOTP::create();
343
        return substr($totp->getSecret(), 0, 16);
344
    }
345
 
346
    /**
347
     * TOTP Factor implementation.
348
     *
349
     * @param stdClass $data
350
     * @return stdClass|null the factor record, or null.
351
     */
352
    public function setup_user_factor(stdClass $data): stdClass|null {
353
        global $DB, $USER;
354
 
355
        if (!empty($data->secret)) {
356
            $row = new stdClass();
357
            $row->userid = $USER->id;
358
            $row->factor = $this->name;
359
            $row->secret = $data->secret;
360
            $row->label = $data->devicename;
361
            $row->timecreated = time();
362
            $row->createdfromip = $USER->lastip;
363
            $row->timemodified = time();
364
            $row->lastverified = 0;
365
            $row->revoked = 0;
366
 
367
            // Check if a record with this configuration already exists, warning the user accordingly.
368
            $record = $DB->get_record('tool_mfa', [
369
                'userid' => $row->userid,
370
                'secret' => $row->secret,
371
                'factor' => $row->factor,
372
            ], '*', IGNORE_MULTIPLE);
373
            if ($record) {
374
                \core\notification::warning(get_string('error:alreadyregistered', 'factor_totp'));
375
                return null;
376
            }
377
 
378
            $id = $DB->insert_record('tool_mfa', $row);
379
            $record = $DB->get_record('tool_mfa', ['id' => $id]);
380
            $this->create_event_after_factor_setup($USER);
381
 
382
            return $record;
383
        }
384
 
385
        return null;
386
    }
387
 
388
    /**
389
     * TOTP Factor implementation with replacement of existing factor.
390
     *
391
     * @param stdClass $data The new factor data.
392
     * @param int $id The id of the factor to replace.
393
     * @return stdClass|null the factor record, or null.
394
     */
395
    public function replace_user_factor(stdClass $data, int $id): stdClass|null {
396
        global $DB, $USER;
397
 
398
        $oldrecord = $DB->get_record('tool_mfa', ['id' => $id]);
399
        $newrecord = null;
400
 
401
        // Ensure we have a valid existing record before setting the new one.
402
        if ($oldrecord) {
403
            $newrecord = $this->setup_user_factor($data);
404
        }
405
        // Ensure the new record was created before revoking the old.
406
        if ($newrecord) {
407
            $this->revoke_user_factor($id);
408
        } else {
409
            \core\notification::warning(get_string('error:couldnotreplace', 'tool_mfa'));
410
            return null;
411
        }
412
        $this->create_event_after_factor_setup($USER);
413
 
414
        return $newrecord ?? null;
415
    }
416
 
417
    /**
418
     * TOTP Factor implementation.
419
     *
420
     * @param stdClass $user the user to check against.
421
     * @return array
422
     */
423
    public function get_all_user_factors($user): array {
424
        global $DB;
425
        return $DB->get_records('tool_mfa', ['userid' => $user->id, 'factor' => $this->name]);
426
    }
427
 
428
    /**
429
     * TOTP Factor implementation.
430
     *
431
     * {@inheritDoc}
432
     */
433
    public function has_revoke(): bool {
434
        return true;
435
    }
436
 
437
    /**
438
     * TOTP Factor implementation.
439
     */
440
    public function has_replace(): bool {
441
        return true;
442
    }
443
 
444
    /**
445
     * TOTP Factor implementation.
446
     *
447
     * {@inheritDoc}
448
     */
449
    public function has_setup(): bool {
450
        return true;
451
    }
452
 
453
    /**
454
     * TOTP Factor implementation
455
     *
456
     * {@inheritDoc}
457
     */
458
    public function show_setup_buttons(): bool {
459
        return true;
460
    }
461
 
462
    /**
463
     * TOTP Factor implementation.
464
     * Empty override of parent.
465
     *
466
     * {@inheritDoc}
467
     */
468
    public function post_pass_state(): void {
469
        return;
470
    }
471
 
472
    /**
473
     * TOTP Factor implementation.
474
     * TOTP cannot return fail state.
475
     *
476
     * @param stdClass $user
477
     */
478
    public function possible_states(stdClass $user): array {
479
        return [
480
            \tool_mfa\plugininfo\factor::STATE_PASS,
481
            \tool_mfa\plugininfo\factor::STATE_NEUTRAL,
482
            \tool_mfa\plugininfo\factor::STATE_UNKNOWN,
483
        ];
484
    }
485
 
486
    /**
487
     * TOTP Factor implementation.
488
     *
489
     * {@inheritDoc}
490
     */
491
    public function get_setup_string(): string {
492
        return get_string('setupfactorbutton', 'factor_totp');
493
    }
494
 
495
    /**
496
     * Gets the string for manage button on preferences page.
497
     *
498
     * @return string
499
     */
500
    public function get_manage_string(): string {
501
        return get_string('managefactorbutton', 'factor_totp');
502
    }
503
}