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 tool_mfa\local;
18
 
19
/**
20
 * MFA secret management class.
21
 *
22
 * @package     tool_mfa
23
 * @author      Peter Burnett <peterburnett@catalyst-au.net>
24
 * @copyright   Catalyst IT
25
 * @license     http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26
 */
27
class secret_manager {
28
 
29
    /** @var string */
30
    const REVOKED = 'revoked';
31
 
32
    /** @var string */
33
    const VALID = 'valid';
34
 
35
    /** @var string */
36
    const NONVALID = 'nonvalid';
37
 
38
    /** @var string */
39
    private $factor;
40
 
41
    /** @var string|false */
42
    private $sessionid;
43
 
44
    /**
45
     * Initialises a secret manager instance
46
     *
47
     * @param   string $factor
48
     */
49
    public function __construct(string $factor) {
50
        $this->factor = $factor;
51
        $this->sessionid = session_id();
52
    }
53
 
54
    /**
55
     * This function creates or takes a secret, and stores it in the database or session.
56
     *
57
     * @param int $expires the length of time the secret is valid. e.g. 1 min = 60
58
     * @param bool $session whether this secret should be linked to the session.
59
     * @param string $secret an optional provided secret
60
     * @return string the secret code, or 0 if no new code created.
61
     */
62
    public function create_secret(int $expires, bool $session, string $secret = null): string {
63
        // Check if there already an active secret, unless we are forcibly given a code.
64
        if ($this->has_active_secret($session) && empty($secret)) {
65
            return '';
66
        }
67
 
68
        // Setup a secret if not provided.
69
        if (empty($secret)) {
70
            $secret = random_int(100000, 999999);
71
        }
72
 
73
        // Now pass the code where it needs to go.
74
        if ($session) {
75
            $this->add_secret_to_db($secret, $expires, $this->sessionid);
76
        } else {
77
            $this->add_secret_to_db($secret, $expires);
78
        }
79
 
80
        return $secret;
81
    }
82
 
83
    /**
84
     * Inserts the provided secret into the database with a given expiry duration.
85
     *
86
     * @param string $secret the secret to store
87
     * @param int $expires expiry duration in seconds
88
     * @param string $sessionid an optional sessionID to tie this record to
89
     * @return void
90
     */
91
    private function add_secret_to_db(string $secret, int $expires, string $sessionid = null): void {
92
        global $DB, $USER;
93
        $expirytime = time() + $expires;
94
 
95
        $data = [
96
            'userid' => $USER->id,
97
            'factor' => $this->factor,
98
            'secret' => $secret,
99
            'timecreated' => time(),
100
            'expiry' => $expirytime,
101
            'revoked' => 0,
102
        ];
103
        if (!empty($sessionid)) {
104
            $data['sessionid'] = $sessionid;
105
        }
106
        $DB->insert_record('tool_mfa_secrets', $data);
107
    }
108
 
109
    /**
110
     * Validates whether the provided secret is currently valid.
111
     *
112
     * @param string $secret the secret to check
113
     * @param bool $keep should the secret be kept for reuse until expiry?
114
     * @return string a secret manager state constant
115
     */
116
    public function validate_secret(string $secret, bool $keep = false): string {
117
        global $DB, $USER;
118
        $status = $this->check_secret_against_db($secret, $this->sessionid);
119
        if ($status !== self::NONVALID) {
120
            if ($status === self::VALID && !$keep) {
121
                // Cleanup DB $record.
122
                $DB->delete_records('tool_mfa_secrets', ['userid' => $USER->id, 'factor' => $this->factor]);
123
            }
124
            return $status;
125
        }
126
        // This is always nonvalid.
127
        return $status;
128
    }
129
 
130
    /**
131
     * Checks if a given secret is valid from the Database.
132
     *
133
     * @param string $secret the secret to check.
134
     * @param string $sessionid the session id to check for.
135
     * @return string a secret manager state constant.
136
     */
137
    private function check_secret_against_db(string $secret, string $sessionid): string {
138
        global $DB, $USER;
139
 
140
        $sql = "SELECT *
141
                  FROM {tool_mfa_secrets}
142
                 WHERE secret = :secret
143
                   AND expiry > :now
144
                   AND userid = :userid
145
                   AND factor = :factor";
146
 
147
        $params = [
148
            'secret' => $secret,
149
            'now' => time(),
150
            'userid' => $USER->id,
151
            'factor' => $this->factor,
152
        ];
153
 
154
        $record = $DB->get_record_sql($sql, $params);
155
 
156
        if (!empty($record)) {
157
            // If revoked it should always be revoked status.
158
            if ($record->revoked) {
159
                return self::REVOKED;
160
            }
161
 
162
            // Check if this is valid in only one session.
163
            if (!empty($record->sessionid)) {
164
                if ($record->sessionid === $sessionid) {
165
                    return self::VALID;
166
                }
167
                return self::NONVALID;
168
            }
169
            return self::VALID;
170
        }
171
        return self::NONVALID;
172
    }
173
 
174
    /**
175
     * Revokes the provided secret code for the user.
176
     *
177
     * @param string $secret the secret to revoke.
178
     * @param int $userid the userid to revoke the secret for.
179
     * @return void
180
     */
181
    public function revoke_secret(string $secret, $userid = null): void {
182
        global $DB, $USER;
183
 
184
        $userid = $userid ?? $USER->id;
185
 
186
        // We do not need to worry about session vs global here.
187
        // A factor should only ever use one.
188
        // We know this secret is valid, so we don't need to check expiry.
189
        $DB->set_field('tool_mfa_secrets', 'revoked', 1, ['userid' => $userid, 'factor' => $this->factor, 'secret' => $secret]);
190
    }
191
 
192
    /**
193
     * Checks whether this factor currently has an active secret, and should not add another.
194
     *
195
     * @param bool $checksession should we only check if a current session secret is active?
196
     * @return bool
197
     */
198
    private function has_active_secret(bool $checksession = false): bool {
199
        global $DB, $USER;
200
 
201
        $sql = "SELECT *
202
                  FROM {tool_mfa_secrets}
203
                 WHERE expiry > :now
204
                   AND userid = :userid
205
                   AND factor = :factor
206
                   AND revoked = 0";
207
 
208
        $params = [
209
            'now' => time(),
210
            'userid' => $USER->id,
211
            'factor' => $this->factor,
212
        ];
213
 
214
        if ($checksession) {
215
            $sql .= ' AND sessionid = :sessionid';
216
            $params['sessionid'] = $this->sessionid;
217
        }
218
 
219
        if ($DB->record_exists_sql($sql, $params)) {
220
            return true;
221
        }
222
 
223
        return false;
224
    }
225
 
226
    /**
227
     * Deletes any user secrets hanging around in the database.
228
     *
229
     * @param int $userid the userid to cleanup temp secrets for.
230
     * @return void
231
     */
232
    public function cleanup_temp_secrets($userid = null): void {
233
        global $DB, $USER;
234
        // Session records are autocleaned up.
235
        // Only DB cleanup required.
236
 
237
        $userid = $userid ?? $USER->id;
238
        $sql = 'DELETE FROM {tool_mfa_secrets}
239
                      WHERE userid = :userid
240
                        AND factor = :factor';
241
 
242
        $DB->execute($sql, ['userid' => $userid, 'factor' => $this->factor]);
243
    }
244
}