Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | 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 {
1441 ariadna 27
 
1 efrain 28
    /** @var string Encryption method: Sodium */
29
    const METHOD_SODIUM = 'sodium';
30
 
31
    /**
32
     * @deprecated since Moodle 4.3 Sodium is always present
33
     */
1441 ariadna 34
    #[\core\attribute\deprecated(null, reason: 'Sodium is always present', since: '4.3', mdl: 'MDL-71421', final: true)]
35
    public static function is_sodium_installed() {
36
        \core\deprecation::emit_deprecation([self::class, __FUNCTION__]);
1 efrain 37
    }
38
 
39
    /**
40
     * Gets the encryption method to use
41
     *
42
     * @return string Current encryption method
43
     */
44
    protected static function get_encryption_method(): string {
45
        return self::METHOD_SODIUM;
46
    }
47
 
48
    /**
49
     * Creates a key for the server.
50
     *
51
     * @param string|null $method Encryption method (only if you want to create a non-default key)
52
     * @param bool $chmod If true, restricts the file access of the key
53
     * @throws \moodle_exception If the server already has a key, or there is an error
54
     */
55
    public static function create_key(?string $method = null, bool $chmod = true): void {
56
        if ($method === null) {
57
            $method = self::get_encryption_method();
58
        }
59
 
60
        if (self::key_exists($method)) {
61
            throw new \moodle_exception('encryption_keyalreadyexists', 'error');
62
        }
63
 
1441 ariadna 64
        // Don't make it read-only in tests or it will fail to clear for future runs.
65
        if (defined('BEHAT_SITE_RUNNING') || PHPUNIT_TEST) {
1 efrain 66
            $chmod = false;
67
        }
68
 
69
        // Generate the key.
70
        switch ($method) {
71
            case self::METHOD_SODIUM:
72
                $key = sodium_crypto_secretbox_keygen();
73
                break;
74
            default:
75
                throw new \coding_exception('Unknown method: ' . $method);
76
        }
77
 
78
        // Store the key, making it readable only by server.
79
        $folder = self::get_key_folder();
80
        check_dir_exists($folder);
81
        $keyfile = self::get_key_file($method);
82
        file_put_contents($keyfile, $key);
83
        if ($chmod) {
84
            chmod($keyfile, 0400);
85
        }
86
    }
87
 
88
    /**
89
     * Gets the folder used to store the secret key.
90
     *
91
     * @return string Folder path
92
     */
93
    protected static function get_key_folder(): string {
94
        global $CFG;
95
        return ($CFG->secretdataroot ?? $CFG->dataroot . '/secret') . '/key';
96
    }
97
 
98
    /**
99
     * Gets the file path used to store the secret key. The filename contains the cipher method,
100
     * so that if necessary to transition in future it would be possible to have multiple.
101
     *
102
     * @param string|null $method Encryption method (only if you want to get a non-default key)
103
     * @return string Full path to file
104
     */
105
    public static function get_key_file(?string $method = null): string {
106
        if ($method === null) {
107
            $method = self::get_encryption_method();
108
        }
109
 
110
        return self::get_key_folder() . '/' . $method . '.key';
111
    }
112
 
113
    /**
114
     * Checks if there is a key file.
115
     *
116
     * @param string|null $method Encryption method (only if you want to check a non-default key)
117
     * @return bool True if there is a key file
118
     */
119
    public static function key_exists(?string $method = null): bool {
120
        if ($method === null) {
121
            $method = self::get_encryption_method();
122
        }
123
 
124
        return file_exists(self::get_key_file($method));
125
    }
126
 
127
    /**
128
     * Gets the current key, automatically creating it if there isn't one yet.
129
     *
130
     * @param string|null $method Encryption method (only if you want to get a non-default key)
131
     * @return string The key (binary)
132
     * @throws \moodle_exception If there isn't one already (and creation is disabled)
133
     */
134
    protected static function get_key(?string $method = null): string {
135
        global $CFG;
136
 
137
        if ($method === null) {
138
            $method = self::get_encryption_method();
139
        }
140
 
141
        $keyfile = self::get_key_file($method);
142
        if (!file_exists($keyfile) && empty($CFG->nokeygeneration)) {
143
            self::create_key($method);
144
        }
145
        $result = @file_get_contents($keyfile);
146
        if ($result === false) {
147
            throw new \moodle_exception('encryption_nokey', 'error');
148
        }
149
        return $result;
150
    }
151
 
152
    /**
153
     * Gets the length in bytes of the initial values data required.
154
     *
155
     * @param string $method Crypto method
156
     * @return int Length in bytes
157
     */
158
    protected static function get_iv_length(string $method): int {
159
        switch ($method) {
160
            case self::METHOD_SODIUM:
161
                return SODIUM_CRYPTO_SECRETBOX_NONCEBYTES;
162
            default:
163
                throw new \coding_exception('Unknown method: ' . $method);
164
        }
165
    }
166
 
167
    /**
168
     * Encrypts data using the server's key.
169
     *
170
     * Note there is a special case - the empty string is not encrypted.
171
     *
172
     * @param string $data Data to encrypt, or empty string for no data
173
     * @param string|null $method Encryption method (only if you want to use a non-default method)
174
     * @return string Encrypted data, or empty string for no data
175
     * @throws \moodle_exception If the key doesn't exist, or the string is too long
176
     */
177
    public static function encrypt(string $data, ?string $method = null): string {
178
        if ($data === '') {
179
            return '';
180
        } else {
181
            if ($method === null) {
182
                $method = self::get_encryption_method();
183
            }
184
 
185
            // Create IV.
186
            $iv = random_bytes(self::get_iv_length($method));
187
 
188
            // Encrypt data.
189
            switch($method) {
190
                case self::METHOD_SODIUM:
191
                    try {
192
                        $encrypted = sodium_crypto_secretbox($data, $iv, self::get_key($method));
193
                    } catch (\SodiumException $e) {
194
                        throw new \moodle_exception('encryption_encryptfailed', 'error', '', null, $e->getMessage());
195
                    }
196
                    break;
197
 
198
                default:
199
                    throw new \coding_exception('Unknown method: ' . $method);
200
            }
201
 
202
            // Encrypted data is cipher method plus IV plus encrypted data.
203
            return $method . ':' . base64_encode($iv . $encrypted);
204
        }
205
    }
206
 
207
    /**
208
     * Decrypts data using the server's key. The decryption works with either supported method.
209
     *
210
     * @param string $data Data to decrypt
211
     * @return string Decrypted data
212
     */
213
    public static function decrypt(string $data): string {
214
        if ($data === '') {
215
            return '';
216
        } else {
1441 ariadna 217
            if (preg_match('~^(' . self::METHOD_SODIUM . '):~', $data, $matches)) {
1 efrain 218
                $method = $matches[1];
219
            } else {
220
                throw new \moodle_exception('encryption_wrongmethod', 'error');
221
            }
222
            $realdata = base64_decode(substr($data, strlen($method) + 1), true);
223
            if ($realdata === false) {
224
                throw new \moodle_exception('encryption_decryptfailed', 'error',
225
                        '', null, 'Invalid base64 data');
226
            }
227
 
228
            $ivlength = self::get_iv_length($method);
229
            if (strlen($realdata) < $ivlength + 1) {
230
                throw new \moodle_exception('encryption_decryptfailed', 'error',
231
                        '', null, 'Insufficient data');
232
            }
233
            $iv = substr($realdata, 0, $ivlength);
234
            $encrypted = substr($realdata, $ivlength);
235
 
236
            switch ($method) {
237
                case self::METHOD_SODIUM:
238
                    try {
239
                        $decrypted = sodium_crypto_secretbox_open($encrypted, $iv, self::get_key($method));
240
                    } catch (\SodiumException $e) {
241
                        throw new \moodle_exception('encryption_decryptfailed', 'error',
242
                                '', null, $e->getMessage());
243
                    }
244
                    // Sodium returns false if decryption fails because data is invalid.
245
                    if ($decrypted === false) {
246
                        throw new \moodle_exception('encryption_decryptfailed', 'error',
247
                                '', null, 'Integrity check failed');
248
                    }
249
                    break;
250
                default:
251
                    throw new \coding_exception('Unknown method: ' . $method);
252
            }
253
 
254
            return $decrypted;
255
        }
256
    }
257
}