2 // This file is part of Moodle - http://moodle.org/
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.
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/>.
21 * @copyright 2020 The Open University
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
31 * @copyright 2020 The Open University
32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 class encryption_test
extends \basic_testcase
{
37 * Clear junk created by tests.
39 protected function tearDown(): void
{
41 $keyfile = encryption
::get_key_file(encryption
::METHOD_OPENSSL
);
42 if (file_exists($keyfile)) {
43 chmod($keyfile, 0700);
45 $keyfile = encryption
::get_key_file(encryption
::METHOD_SODIUM
);
46 if (file_exists($keyfile)) {
47 chmod($keyfile, 0700);
49 remove_dir($CFG->dataroot
. '/secret');
50 unset($CFG->nokeygeneration
);
53 protected function setUp(): void
{
56 require_once(__DIR__
. '/fixtures/testable_encryption.php');
60 * Tests using Sodium need to check the extension is available.
62 * @param string $method Encryption method
64 protected function require_sodium(string $method) {
65 if ($method == encryption
::METHOD_SODIUM
) {
66 if (!encryption
::is_sodium_installed()) {
67 $this->markTestSkipped('Sodium not installed');
73 * Many of the tests work with both encryption methods.
75 * @return array[] Array of method options for test
77 public function encryption_method_provider(): array {
78 return ['Sodium' => [encryption
::METHOD_SODIUM
], 'OpenSSL' => [encryption
::METHOD_OPENSSL
]];
82 * Tests the create_keys and get_key functions.
84 * @param string $method Encryption method
85 * @dataProvider encryption_method_provider
87 public function test_create_key(string $method): void
{
88 $this->require_sodium($method);
89 encryption
::create_key($method);
90 $key = testable_encryption
::get_key($method);
92 // Conveniently, both encryption methods have the same key length.
93 $this->assertEquals(32, strlen($key));
95 $this->expectExceptionMessage('Key already exists');
96 encryption
::create_key($method);
100 * Tests encryption and decryption with empty strings.
102 * @throws \moodle_exception
104 public function test_encrypt_and_decrypt_empty(): void
{
105 $this->assertEquals('', encryption
::encrypt(''));
106 $this->assertEquals('', encryption
::decrypt(''));
110 * Tests encryption when the keys weren't created yet.
112 * @param string $method Encryption method
113 * @dataProvider encryption_method_provider
115 public function test_encrypt_nokeys(string $method): void
{
117 $this->require_sodium($method);
119 // Prevent automatic generation of keys.
120 $CFG->nokeygeneration
= true;
121 $this->expectExceptionMessage('Key not found');
122 encryption
::encrypt('frogs', $method);
126 * Tests decryption when the data has a different encryption method
128 public function test_decrypt_wrongmethod(): void
{
129 $this->expectExceptionMessage('Data does not match a supported encryption method');
130 encryption
::decrypt('FAKE-CIPHER-METHOD:xx');
134 * Tests decryption when not enough data is supplied to get the IV and some data.
136 * @dataProvider encryption_method_provider
137 * @param string $method Encryption method
139 public function test_decrypt_tooshort(string $method): void
{
140 $this->require_sodium($method);
142 $this->expectExceptionMessage('Insufficient data');
144 case encryption
::METHOD_OPENSSL
:
145 // It needs min 49 bytes (16 bytes IV + 32 bytes HMAC + 1 byte data).
146 $justtooshort = '0123456789abcdef0123456789abcdef0123456789abcdef';
148 case encryption
::METHOD_SODIUM
:
149 // Sodium needs 25 bytes at least as far as our code is concerned (24 bytes IV + 1
150 // byte data); it splits out any authentication hashes itself.
151 $justtooshort = '0123456789abcdef01234567';
155 encryption
::decrypt($method . ':' .base64_encode($justtooshort));
159 * Tests decryption when data is not valid base64.
161 * @dataProvider encryption_method_provider
162 * @param string $method Encryption method
164 public function test_decrypt_notbase64(string $method): void
{
165 $this->require_sodium($method);
167 $this->expectExceptionMessage('Invalid base64 data');
168 encryption
::decrypt($method . ':' . chr(160));
172 * Tests decryption when the keys weren't created yet.
174 * @dataProvider encryption_method_provider
175 * @param string $method Encryption method
177 public function test_decrypt_nokeys(string $method): void
{
179 $this->require_sodium($method);
181 // Prevent automatic generation of keys.
182 $CFG->nokeygeneration
= true;
183 $this->expectExceptionMessage('Key not found');
184 encryption
::decrypt($method . ':' . base64_encode(
185 '0123456789abcdef0123456789abcdef0123456789abcdef0'));
189 * Test automatic generation of keys when needed.
191 * @dataProvider encryption_method_provider
192 * @param string $method Encryption method
194 public function test_auto_key_generation(string $method): void
{
195 $this->require_sodium($method);
197 // Allow automatic generation (default).
198 $encrypted = encryption
::encrypt('frogs', $method);
199 $this->assertEquals('frogs', encryption
::decrypt($encrypted));
203 * Checks that invalid key causes failures.
205 * @dataProvider encryption_method_provider
206 * @param string $method Encryption method
208 public function test_invalid_key(string $method): void
{
210 $this->require_sodium($method);
212 // Set the key to something bogus.
213 $folder = $CFG->dataroot
. '/secret/key';
214 check_dir_exists($folder);
215 file_put_contents(encryption
::get_key_file($method), 'silly');
218 case encryption
::METHOD_SODIUM
:
219 $this->expectExceptionMessageMatches('/(should|must) be SODIUM_CRYPTO_SECRETBOX_KEYBYTES bytes/');
222 case encryption
::METHOD_OPENSSL
:
223 $this->expectExceptionMessage('Invalid key');
226 encryption
::encrypt('frogs', $method);
230 * Checks that modified data causes failures.
232 * @dataProvider encryption_method_provider
233 * @param string $method Encryption method
235 public function test_modified_data(string $method): void
{
236 $this->require_sodium($method);
238 $encrypted = encryption
::encrypt('frogs', $method);
239 $mainbit = base64_decode(substr($encrypted, strlen($method) +
1));
240 $mainbit = substr($mainbit, 0, 16) . 'X' . substr($mainbit, 16);
241 $encrypted = $method . ':' . base64_encode($mainbit);
242 $this->expectExceptionMessage('Integrity check failed');
243 encryption
::decrypt($encrypted);
247 * Tests encryption and decryption for real.
249 * @dataProvider encryption_method_provider
250 * @param string $method Encryption method
251 * @throws \moodle_exception
253 public function test_encrypt_and_decrypt_realdata(string $method): void
{
254 $this->require_sodium($method);
256 // Encrypt short string.
257 $encrypted = encryption
::encrypt('frogs', $method);
258 $this->assertNotEquals('frogs', $encrypted);
259 $this->assertEquals('frogs', encryption
::decrypt($encrypted));
261 // Encrypt really long string (1 MB).
262 $long = str_repeat('X', 1024 * 1024);
263 $this->assertEquals($long, encryption
::decrypt(encryption
::encrypt($long, $method)));