MDL-71421 core: deprecate openssl fallbacks in encryption library.
[moodle.git] / lib / tests / encryption_test.php
blobf106159bd30f547d33a5ea5416f18b265e83d9ec
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.
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/>.
17 namespace core;
19 use advanced_testcase;
21 /**
22 * Test encryption.
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
29 class encryption_test extends advanced_testcase {
31 /**
32 * Clear junk created by tests.
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);
40 $keyfile = encryption::get_key_file(encryption::METHOD_SODIUM);
41 if (file_exists($keyfile)) {
42 chmod($keyfile, 0700);
44 remove_dir($CFG->dataroot . '/secret');
45 unset($CFG->nokeygeneration);
48 protected function setUp(): void {
49 $this->tearDown();
51 require_once(__DIR__ . '/fixtures/testable_encryption.php');
54 /**
55 * Many of the tests work with both encryption methods.
57 * @return array[] Array of method options for test
59 public function encryption_method_provider(): array {
60 return [
61 'Sodium' => [encryption::METHOD_SODIUM],
65 /**
66 * Tests the create_keys and get_key functions.
68 * @param string $method Encryption method
69 * @dataProvider encryption_method_provider
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));
76 $this->expectExceptionMessage('Key already exists');
77 encryption::create_key($method);
80 /**
81 * Test that we can create keys for legacy {@see encryption::METHOD_OPENSSL} content
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));
88 $this->expectExceptionMessage('Key already exists');
89 encryption::create_key(encryption::METHOD_OPENSSL);
92 /**
93 * Tests encryption and decryption with empty strings.
95 public function test_encrypt_and_decrypt_empty(): void {
96 $this->assertEquals('', encryption::encrypt(''));
97 $this->assertEquals('', encryption::decrypt(''));
101 * Tests encryption when the keys weren't created yet.
103 * @param string $method Encryption method
104 * @dataProvider encryption_method_provider
106 public function test_encrypt_nokeys(string $method): void {
107 global $CFG;
109 // Prevent automatic generation of keys.
110 $CFG->nokeygeneration = true;
111 $this->expectExceptionMessage('Key not found');
112 encryption::encrypt('frogs', $method);
116 * Test that attempting to encrypt with legacy {@see encryption::METHOD_OPENSSL} method falls back to Sodium
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']);
125 * Tests decryption when the data has a different encryption method
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');
133 * Tests decryption when not enough data is supplied to get the IV and some data.
135 * @dataProvider encryption_method_provider
136 * @param string $method Encryption method
138 public function test_decrypt_tooshort(string $method): void {
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;
153 encryption::decrypt($method . ':' .base64_encode($justtooshort));
157 * Tests decryption when data is not valid base64.
159 * @dataProvider encryption_method_provider
160 * @param string $method Encryption method
162 public function test_decrypt_notbase64(string $method): void {
163 $this->expectExceptionMessage('Invalid base64 data');
164 encryption::decrypt($method . ':' . chr(160));
168 * Tests decryption when the keys weren't created yet.
170 * @dataProvider encryption_method_provider
171 * @param string $method Encryption method
173 public function test_decrypt_nokeys(string $method): void {
174 global $CFG;
176 // Prevent automatic generation of keys.
177 $CFG->nokeygeneration = true;
178 $this->expectExceptionMessage('Key not found');
179 encryption::decrypt($method . ':' . base64_encode(
180 '0123456789abcdef0123456789abcdef0123456789abcdef0'));
184 * Test that we can decrypt legacy {@see encryption::METHOD_OPENSSL} content
186 public function test_decrypt_openssl(): void {
187 $key = testable_encryption::get_key(encryption::METHOD_OPENSSL);
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);
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']);
200 * Test automatic generation of keys when needed.
202 * @dataProvider encryption_method_provider
203 * @param string $method Encryption method
205 public function test_auto_key_generation(string $method): void {
207 // Allow automatic generation (default).
208 $encrypted = encryption::encrypt('frogs', $method);
209 $this->assertEquals('frogs', encryption::decrypt($encrypted));
213 * Checks that invalid key causes failures.
215 * @dataProvider encryption_method_provider
216 * @param string $method Encryption method
218 public function test_invalid_key(string $method): void {
219 global $CFG;
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');
226 switch ($method) {
227 case encryption::METHOD_SODIUM:
228 $this->expectExceptionMessageMatches('/(should|must) be SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes/');
229 break;
231 case encryption::METHOD_OPENSSL:
232 $this->expectExceptionMessage('Invalid key');
233 break;
235 encryption::encrypt('frogs', $method);
239 * Checks that modified data causes failures.
241 * @dataProvider encryption_method_provider
242 * @param string $method Encryption method
244 public function test_modified_data(string $method): void {
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);
255 * Tests encryption and decryption for real.
257 * @dataProvider encryption_method_provider
258 * @param string $method Encryption method
260 public function test_encrypt_and_decrypt_realdata(string $method): void {
262 // Encrypt short string.
263 $encrypted = encryption::encrypt('frogs', $method);
264 $this->assertNotEquals('frogs', $encrypted);
265 $this->assertEquals('frogs', encryption::decrypt($encrypted));
267 // Encrypt really long string (1 MB).
268 $long = str_repeat('X', 1024 * 1024);
269 $this->assertEquals($long, encryption::decrypt(encryption::encrypt($long, $method)));