Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>.namespace tool_mfa\local;/*** MFA secret management class.** @package tool_mfa* @author Peter Burnett <peterburnett@catalyst-au.net>* @copyright Catalyst IT* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class secret_manager {/** @var string */const REVOKED = 'revoked';/** @var string */const VALID = 'valid';/** @var string */const NONVALID = 'nonvalid';/** @var string */private $factor;/** @var string|false */private $sessionid;/*** Initialises a secret manager instance** @param string $factor*/public function __construct(string $factor) {$this->factor = $factor;$this->sessionid = session_id();}/*** This function creates or takes a secret, and stores it in the database or session.** @param int $expires the length of time the secret is valid. e.g. 1 min = 60* @param bool $session whether this secret should be linked to the session.* @param string $secret an optional provided secret* @return string the secret code, or 0 if no new code created.*/public function create_secret(int $expires, bool $session, string $secret = null): string {// Check if there already an active secret, unless we are forcibly given a code.if ($this->has_active_secret($session) && empty($secret)) {return '';}// Setup a secret if not provided.if (empty($secret)) {$secret = random_int(100000, 999999);}// Now pass the code where it needs to go.if ($session) {$this->add_secret_to_db($secret, $expires, $this->sessionid);} else {$this->add_secret_to_db($secret, $expires);}return $secret;}/*** Inserts the provided secret into the database with a given expiry duration.** @param string $secret the secret to store* @param int $expires expiry duration in seconds* @param string $sessionid an optional sessionID to tie this record to* @return void*/private function add_secret_to_db(string $secret, int $expires, string $sessionid = null): void {global $DB, $USER;$expirytime = time() + $expires;$data = ['userid' => $USER->id,'factor' => $this->factor,'secret' => $secret,'timecreated' => time(),'expiry' => $expirytime,'revoked' => 0,];if (!empty($sessionid)) {$data['sessionid'] = $sessionid;}$DB->insert_record('tool_mfa_secrets', $data);}/*** Validates whether the provided secret is currently valid.** @param string $secret the secret to check* @param bool $keep should the secret be kept for reuse until expiry?* @return string a secret manager state constant*/public function validate_secret(string $secret, bool $keep = false): string {global $DB, $USER;$status = $this->check_secret_against_db($secret, $this->sessionid);if ($status !== self::NONVALID) {if ($status === self::VALID && !$keep) {// Cleanup DB $record.$DB->delete_records('tool_mfa_secrets', ['userid' => $USER->id, 'factor' => $this->factor]);}return $status;}// This is always nonvalid.return $status;}/*** Checks if a given secret is valid from the Database.** @param string $secret the secret to check.* @param string $sessionid the session id to check for.* @return string a secret manager state constant.*/private function check_secret_against_db(string $secret, string $sessionid): string {global $DB, $USER;$sql = "SELECT *FROM {tool_mfa_secrets}WHERE secret = :secretAND expiry > :nowAND userid = :useridAND factor = :factor";$params = ['secret' => $secret,'now' => time(),'userid' => $USER->id,'factor' => $this->factor,];$record = $DB->get_record_sql($sql, $params);if (!empty($record)) {// If revoked it should always be revoked status.if ($record->revoked) {return self::REVOKED;}// Check if this is valid in only one session.if (!empty($record->sessionid)) {if ($record->sessionid === $sessionid) {return self::VALID;}return self::NONVALID;}return self::VALID;}return self::NONVALID;}/*** Revokes the provided secret code for the user.** @param string $secret the secret to revoke.* @param int $userid the userid to revoke the secret for.* @return void*/public function revoke_secret(string $secret, $userid = null): void {global $DB, $USER;$userid = $userid ?? $USER->id;// We do not need to worry about session vs global here.// A factor should only ever use one.// We know this secret is valid, so we don't need to check expiry.$DB->set_field('tool_mfa_secrets', 'revoked', 1, ['userid' => $userid, 'factor' => $this->factor, 'secret' => $secret]);}/*** Checks whether this factor currently has an active secret, and should not add another.** @param bool $checksession should we only check if a current session secret is active?* @return bool*/private function has_active_secret(bool $checksession = false): bool {global $DB, $USER;$sql = "SELECT *FROM {tool_mfa_secrets}WHERE expiry > :nowAND userid = :useridAND factor = :factorAND revoked = 0";$params = ['now' => time(),'userid' => $USER->id,'factor' => $this->factor,];if ($checksession) {$sql .= ' AND sessionid = :sessionid';$params['sessionid'] = $this->sessionid;}if ($DB->record_exists_sql($sql, $params)) {return true;}return false;}/*** Deletes any user secrets hanging around in the database.** @param int $userid the userid to cleanup temp secrets for.* @return void*/public function cleanup_temp_secrets($userid = null): void {global $DB, $USER;// Session records are autocleaned up.// Only DB cleanup required.$userid = $userid ?? $USER->id;$sql = 'DELETE FROM {tool_mfa_secrets}WHERE userid = :useridAND factor = :factor';$DB->execute($sql, ['userid' => $userid, 'factor' => $this->factor]);}}