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 core;
18
 
19
/**
20
 * Class used to encrypt or decrypt data.
21
 *
22
 * @package core
23
 * @copyright 2020 The Open University
24
 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25
 */
26
class encryption {
27
    /** @var string Encryption method: Sodium */
28
    const METHOD_SODIUM = 'sodium';
29
 
30
    /**
31
     * @var string Encryption method: hand-coded OpenSSL (less safe)
32
     *
33
     * @deprecated
34
     */
35
    const METHOD_OPENSSL = 'openssl-aes-256-ctr';
36
 
37
    /**
38
     * @var string OpenSSL cipher method
39
     *
40
     * @deprecated
41
     */
42
    const OPENSSL_CIPHER = 'AES-256-CTR';
43
 
44
    /**
45
     * Checks if Sodium is installed.
46
     *
47
     * @return bool True if the Sodium extension is available
48
     *
49
     * @deprecated since Moodle 4.3 Sodium is always present
50
     */
51
    public static function is_sodium_installed(): bool {
52
        debugging(__FUNCTION__ . '() is deprecated, sodium is now always present', DEBUG_DEVELOPER);
53
        return extension_loaded('sodium');
54
    }
55
 
56
    /**
57
     * Gets the encryption method to use
58
     *
59
     * @return string Current encryption method
60
     */
61
    protected static function get_encryption_method(): string {
62
        return self::METHOD_SODIUM;
63
    }
64
 
65
    /**
66
     * Creates a key for the server.
67
     *
68
     * Note we currently retain support for all methods, in order to decrypt legacy {@see METHOD_OPENSSL} content
69
     *
70
     * @param string|null $method Encryption method (only if you want to create a non-default key)
71
     * @param bool $chmod If true, restricts the file access of the key
72
     * @throws \moodle_exception If the server already has a key, or there is an error
73
     */
74
    public static function create_key(?string $method = null, bool $chmod = true): void {
75
        if ($method === null) {
76
            $method = self::get_encryption_method();
77
        }
78
 
79
        if (self::key_exists($method)) {
80
            throw new \moodle_exception('encryption_keyalreadyexists', 'error');
81
        }
82
 
83
        // Don't make it read-only in Behat or it will fail to clear for future runs.
84
        if (defined('BEHAT_SITE_RUNNING')) {
85
            $chmod = false;
86
        }
87
 
88
        // Generate the key.
89
        switch ($method) {
90
            case self::METHOD_SODIUM:
91
                $key = sodium_crypto_secretbox_keygen();
92
                break;
93
            case self::METHOD_OPENSSL:
94
                $key = openssl_random_pseudo_bytes(32);
95
                break;
96
            default:
97
                throw new \coding_exception('Unknown method: ' . $method);
98
        }
99
 
100
        // Store the key, making it readable only by server.
101
        $folder = self::get_key_folder();
102
        check_dir_exists($folder);
103
        $keyfile = self::get_key_file($method);
104
        file_put_contents($keyfile, $key);
105
        if ($chmod) {
106
            chmod($keyfile, 0400);
107
        }
108
    }
109
 
110
    /**
111
     * Gets the folder used to store the secret key.
112
     *
113
     * @return string Folder path
114
     */
115
    protected static function get_key_folder(): string {
116
        global $CFG;
117
        return ($CFG->secretdataroot ?? $CFG->dataroot . '/secret') . '/key';
118
    }
119
 
120
    /**
121
     * Gets the file path used to store the secret key. The filename contains the cipher method,
122
     * so that if necessary to transition in future it would be possible to have multiple.
123
     *
124
     * @param string|null $method Encryption method (only if you want to get a non-default key)
125
     * @return string Full path to file
126
     */
127
    public static function get_key_file(?string $method = null): string {
128
        if ($method === null) {
129
            $method = self::get_encryption_method();
130
        }
131
 
132
        return self::get_key_folder() . '/' . $method . '.key';
133
    }
134
 
135
    /**
136
     * Checks if there is a key file.
137
     *
138
     * @param string|null $method Encryption method (only if you want to check a non-default key)
139
     * @return bool True if there is a key file
140
     */
141
    public static function key_exists(?string $method = null): bool {
142
        if ($method === null) {
143
            $method = self::get_encryption_method();
144
        }
145
 
146
        return file_exists(self::get_key_file($method));
147
    }
148
 
149
    /**
150
     * Gets the current key, automatically creating it if there isn't one yet.
151
     *
152
     * @param string|null $method Encryption method (only if you want to get a non-default key)
153
     * @return string The key (binary)
154
     * @throws \moodle_exception If there isn't one already (and creation is disabled)
155
     */
156
    protected static function get_key(?string $method = null): string {
157
        global $CFG;
158
 
159
        if ($method === null) {
160
            $method = self::get_encryption_method();
161
        }
162
 
163
        $keyfile = self::get_key_file($method);
164
        if (!file_exists($keyfile) && empty($CFG->nokeygeneration)) {
165
            self::create_key($method);
166
        }
167
        $result = @file_get_contents($keyfile);
168
        if ($result === false) {
169
            throw new \moodle_exception('encryption_nokey', 'error');
170
        }
171
        return $result;
172
    }
173
 
174
    /**
175
     * Gets the length in bytes of the initial values data required.
176
     *
177
     * Note we currently retain support for all methods, in order to decrypt legacy {@see METHOD_OPENSSL} content
178
     *
179
     * @param string $method Crypto method
180
     * @return int Length in bytes
181
     */
182
    protected static function get_iv_length(string $method): int {
183
        switch ($method) {
184
            case self::METHOD_SODIUM:
185
                return SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
186
            case self::METHOD_OPENSSL:
187
                return openssl_cipher_iv_length(self::OPENSSL_CIPHER);
188
            default:
189
                throw new \coding_exception('Unknown method: ' . $method);
190
        }
191
    }
192
 
193
    /**
194
     * Encrypts data using the server's key.
195
     *
196
     * Note there is a special case - the empty string is not encrypted.
197
     *
198
     * @param string $data Data to encrypt, or empty string for no data
199
     * @param string|null $method Encryption method (only if you want to use a non-default method)
200
     * @return string Encrypted data, or empty string for no data
201
     * @throws \moodle_exception If the key doesn't exist, or the string is too long
202
     */
203
    public static function encrypt(string $data, ?string $method = null): string {
204
        if ($data === '') {
205
            return '';
206
        } else {
207
            if ($method === null) {
208
                $method = self::get_encryption_method();
209
            }
210
 
211
            // We currently retain support for all methods, falling back to Sodium if deprecated OpenSSL is requested.
212
            if ($method === self::METHOD_OPENSSL) {
213
                debugging('Encryption using legacy OpenSSL is deprecated, reverting to Sodium', DEBUG_DEVELOPER);
214
                $method = self::METHOD_SODIUM;
215
            }
216
 
217
            // Create IV.
218
            $iv = random_bytes(self::get_iv_length($method));
219
 
220
            // Encrypt data.
221
            switch($method) {
222
                case self::METHOD_SODIUM:
223
                    try {
224
                        $encrypted = sodium_crypto_secretbox($data, $iv, self::get_key($method));
225
                    } catch (\SodiumException $e) {
226
                        throw new \moodle_exception('encryption_encryptfailed', 'error', '', null, $e->getMessage());
227
                    }
228
                    break;
229
 
230
                default:
231
                    throw new \coding_exception('Unknown method: ' . $method);
232
            }
233
 
234
            // Encrypted data is cipher method plus IV plus encrypted data.
235
            return $method . ':' . base64_encode($iv . $encrypted);
236
        }
237
    }
238
 
239
    /**
240
     * Decrypts data using the server's key. The decryption works with either supported method.
241
     *
242
     * Note currently we retain support for all methods, in order to decrypt legacy {@see METHOD_OPENSSL} content
243
     *
244
     * @param string $data Data to decrypt
245
     * @return string Decrypted data
246
     */
247
    public static function decrypt(string $data): string {
248
        if ($data === '') {
249
            return '';
250
        } else {
251
            if (preg_match('~^(' . self::METHOD_OPENSSL . '|' . self::METHOD_SODIUM . '):~', $data, $matches)) {
252
                $method = $matches[1];
253
            } else {
254
                throw new \moodle_exception('encryption_wrongmethod', 'error');
255
            }
256
            $realdata = base64_decode(substr($data, strlen($method) + 1), true);
257
            if ($realdata === false) {
258
                throw new \moodle_exception('encryption_decryptfailed', 'error',
259
                        '', null, 'Invalid base64 data');
260
            }
261
 
262
            $ivlength = self::get_iv_length($method);
263
            if (strlen($realdata) < $ivlength + 1) {
264
                throw new \moodle_exception('encryption_decryptfailed', 'error',
265
                        '', null, 'Insufficient data');
266
            }
267
            $iv = substr($realdata, 0, $ivlength);
268
            $encrypted = substr($realdata, $ivlength);
269
 
270
            switch ($method) {
271
                case self::METHOD_SODIUM:
272
                    try {
273
                        $decrypted = sodium_crypto_secretbox_open($encrypted, $iv, self::get_key($method));
274
                    } catch (\SodiumException $e) {
275
                        throw new \moodle_exception('encryption_decryptfailed', 'error',
276
                                '', null, $e->getMessage());
277
                    }
278
                    // Sodium returns false if decryption fails because data is invalid.
279
                    if ($decrypted === false) {
280
                        throw new \moodle_exception('encryption_decryptfailed', 'error',
281
                                '', null, 'Integrity check failed');
282
                    }
283
                    break;
284
 
285
                case self::METHOD_OPENSSL:
286
                    if (strlen($encrypted) < 33) {
287
                        throw new \moodle_exception('encryption_decryptfailed', 'error',
288
                                '', null, 'Insufficient data');
289
                    }
290
                    $hmac = substr($encrypted, -32);
291
                    $encrypted = substr($encrypted, 0, -32);
292
                    $key = self::get_key($method);
293
                    $expectedhmac = hash_hmac('sha256', $iv . $encrypted, $key, true);
294
                    if ($hmac !== $expectedhmac) {
295
                        throw new \moodle_exception('encryption_decryptfailed', 'error',
296
                                '', null, 'Integrity check failed');
297
                    }
298
 
299
                    debugging('Decryption using legacy OpenSSL is deprecated, please upgrade to Sodium', DEBUG_DEVELOPER);
300
 
301
                    $decrypted = @openssl_decrypt($encrypted, self::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv);
302
                    if ($decrypted === false) {
303
                        throw new \moodle_exception('encryption_decryptfailed', 'error',
304
                                '', null, openssl_error_string());
305
                    }
306
                    break;
307
 
308
                default:
309
                    throw new \coding_exception('Unknown method: ' . $method);
310
            }
311
 
312
            return $decrypted;
313
        }
314
    }
315
}