Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 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
}