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 use advanced_testcase; 20 21 /** 22 * Test encryption. 23 * 24 * @package core 25 * @copyright 2020 The Open University 26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 27 * @covers \core\encryption 28 */ 29 class encryption_test extends advanced_testcase { 30 31 /** 32 * Clear junk created by tests. 33 */ 34 protected function tearDown(): void { 35 global $CFG; 36 $keyfile = encryption::get_key_file(encryption::METHOD_OPENSSL); 37 if (file_exists($keyfile)) { 38 chmod($keyfile, 0700); 39 } 40 $keyfile = encryption::get_key_file(encryption::METHOD_SODIUM); 41 if (file_exists($keyfile)) { 42 chmod($keyfile, 0700); 43 } 44 remove_dir($CFG->dataroot . '/secret'); 45 unset($CFG->nokeygeneration); 46 } 47 48 protected function setUp(): void { 49 $this->tearDown(); 50 51 require_once (__DIR__ . '/fixtures/testable_encryption.php'); 52 } 53 54 /** 55 * Many of the tests work with both encryption methods. 56 * 57 * @return array[] Array of method options for test 58 */ 59 public function encryption_method_provider(): array { 60 return [ 61 'Sodium' => [encryption::METHOD_SODIUM], 62 ]; 63 } 64 65 /** 66 * Tests the create_keys and get_key functions. 67 * 68 * @param string $method Encryption method 69 * @dataProvider encryption_method_provider 70 */ 71 public function test_create_key(string $method): void { 72 encryption::create_key($method); 73 $key = testable_encryption::get_key($method); 74 $this->assertEquals(32, strlen($key)); 75 76 $this->expectExceptionMessage('Key already exists'); 77 encryption::create_key($method); 78 } 79 80 /** 81 * Test that we can create keys for legacy {@see encryption::METHOD_OPENSSL} content 82 */ 83 public function test_create_key_openssl(): void { 84 encryption::create_key(encryption::METHOD_OPENSSL); 85 $key = testable_encryption::get_key(encryption::METHOD_OPENSSL); 86 $this->assertEquals(32, strlen($key)); 87 88 $this->expectExceptionMessage('Key already exists'); 89 encryption::create_key(encryption::METHOD_OPENSSL); 90 } 91 92 /** 93 * Tests encryption and decryption with empty strings. 94 */ 95 public function test_encrypt_and_decrypt_empty(): void { 96 $this->assertEquals('', encryption::encrypt('')); 97 $this->assertEquals('', encryption::decrypt('')); 98 } 99 100 /** 101 * Tests encryption when the keys weren't created yet. 102 * 103 * @param string $method Encryption method 104 * @dataProvider encryption_method_provider 105 */ 106 public function test_encrypt_nokeys(string $method): void { 107 global $CFG; 108 109 // Prevent automatic generation of keys. 110 $CFG->nokeygeneration = true; 111 $this->expectExceptionMessage('Key not found'); 112 encryption::encrypt('frogs', $method); 113 } 114 115 /** 116 * Test that attempting to encrypt with legacy {@see encryption::METHOD_OPENSSL} method falls back to Sodium 117 */ 118 public function test_encrypt_openssl(): void { 119 $encrypted = encryption::encrypt('Frogs', encryption::METHOD_OPENSSL); 120 $this->assertStringStartsWith(encryption::METHOD_SODIUM . ':', $encrypted); 121 $this->assertDebuggingCalledCount(1, ['Encryption using legacy OpenSSL is deprecated, reverting to Sodium']); 122 } 123 124 /** 125 * Tests decryption when the data has a different encryption method 126 */ 127 public function test_decrypt_wrongmethod(): void { 128 $this->expectExceptionMessage('Data does not match a supported encryption method'); 129 encryption::decrypt('FAKE-CIPHER-METHOD:xx'); 130 } 131 132 /** 133 * Tests decryption when not enough data is supplied to get the IV and some data. 134 * 135 * @dataProvider encryption_method_provider 136 * @param string $method Encryption method 137 */ 138 public function test_decrypt_tooshort(string $method): void { 139 140 $this->expectExceptionMessage('Insufficient data'); 141 switch ($method) { 142 case encryption::METHOD_OPENSSL: 143 // It needs min 49 bytes (16 bytes IV + 32 bytes HMAC + 1 byte data). 144 $justtooshort = '0123456789abcdef0123456789abcdef0123456789abcdef'; 145 break; 146 case encryption::METHOD_SODIUM: 147 // Sodium needs 25 bytes at least as far as our code is concerned (24 bytes IV + 1 148 // byte data); it splits out any authentication hashes itself. 149 $justtooshort = '0123456789abcdef01234567'; 150 break; 151 } 152 153 encryption::decrypt($method . ':' .base64_encode($justtooshort)); 154 } 155 156 /** 157 * Tests decryption when data is not valid base64. 158 * 159 * @dataProvider encryption_method_provider 160 * @param string $method Encryption method 161 */ 162 public function test_decrypt_notbase64(string $method): void { 163 $this->expectExceptionMessage('Invalid base64 data'); 164 encryption::decrypt($method . ':' . chr(160)); 165 } 166 167 /** 168 * Tests decryption when the keys weren't created yet. 169 * 170 * @dataProvider encryption_method_provider 171 * @param string $method Encryption method 172 */ 173 public function test_decrypt_nokeys(string $method): void { 174 global $CFG; 175 176 // Prevent automatic generation of keys. 177 $CFG->nokeygeneration = true; 178 $this->expectExceptionMessage('Key not found'); 179 encryption::decrypt($method . ':' . base64_encode( 180 '0123456789abcdef0123456789abcdef0123456789abcdef0')); 181 } 182 183 /** 184 * Test that we can decrypt legacy {@see encryption::METHOD_OPENSSL} content 185 */ 186 public function test_decrypt_openssl(): void { 187 $key = testable_encryption::get_key(encryption::METHOD_OPENSSL); 188 189 // Construct encrypted string using openssl method/cipher. 190 $iv = random_bytes(openssl_cipher_iv_length(encryption::OPENSSL_CIPHER)); 191 $encrypted = @openssl_encrypt('Frogs', encryption::OPENSSL_CIPHER, $key, OPENSSL_RAW_DATA, $iv); 192 $hmac = hash_hmac('sha256', $iv . $encrypted, $key, true); 193 194 $decrypted = encryption::decrypt(encryption::METHOD_OPENSSL . ':' . base64_encode($iv . $encrypted . $hmac)); 195 $this->assertEquals('Frogs', $decrypted); 196 $this->assertDebuggingCalledCount(1, ['Decryption using legacy OpenSSL is deprecated, please upgrade to Sodium']); 197 } 198 199 /** 200 * Test automatic generation of keys when needed. 201 * 202 * @dataProvider encryption_method_provider 203 * @param string $method Encryption method 204 */ 205 public function test_auto_key_generation(string $method): void { 206 207 // Allow automatic generation (default). 208 $encrypted = encryption::encrypt('frogs', $method); 209 $this->assertEquals('frogs', encryption::decrypt($encrypted)); 210 } 211 212 /** 213 * Checks that invalid key causes failures. 214 * 215 * @dataProvider encryption_method_provider 216 * @param string $method Encryption method 217 */ 218 public function test_invalid_key(string $method): void { 219 global $CFG; 220 221 // Set the key to something bogus. 222 $folder = $CFG->dataroot . '/secret/key'; 223 check_dir_exists($folder); 224 file_put_contents(encryption::get_key_file($method), 'silly'); 225 226 switch ($method) { 227 case encryption::METHOD_SODIUM: 228 $this->expectExceptionMessageMatches('/(should|must) be SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes/'); 229 break; 230 231 case encryption::METHOD_OPENSSL: 232 $this->expectExceptionMessage('Invalid key'); 233 break; 234 } 235 encryption::encrypt('frogs', $method); 236 } 237 238 /** 239 * Checks that modified data causes failures. 240 * 241 * @dataProvider encryption_method_provider 242 * @param string $method Encryption method 243 */ 244 public function test_modified_data(string $method): void { 245 246 $encrypted = encryption::encrypt('frogs', $method); 247 $mainbit = base64_decode(substr($encrypted, strlen($method) + 1)); 248 $mainbit = substr($mainbit, 0, 16) . 'X' . substr($mainbit, 16); 249 $encrypted = $method . ':' . base64_encode($mainbit); 250 $this->expectExceptionMessage('Integrity check failed'); 251 encryption::decrypt($encrypted); 252 } 253 254 /** 255 * Tests encryption and decryption for real. 256 * 257 * @dataProvider encryption_method_provider 258 * @param string $method Encryption method 259 */ 260 public function test_encrypt_and_decrypt_realdata(string $method): void { 261 262 // Encrypt short string. 263 $encrypted = encryption::encrypt('frogs', $method); 264 $this->assertNotEquals('frogs', $encrypted); 265 $this->assertEquals('frogs', encryption::decrypt($encrypted)); 266 267 // Encrypt really long string (1 MB). 268 $long = str_repeat('X', 1024 * 1024); 269 $this->assertEquals($long, encryption::decrypt(encryption::encrypt($long, $method))); 270 } 271 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body