Differences Between: [Versions 311 and 403] [Versions 400 and 403] [Versions 401 and 403] [Versions 402 and 403]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body