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