blob: 8e411d9fa383c1a07abb8929fb0acc520e6f8c0d [file] [log] [blame]
Andrey Andreev912831f2014-02-04 17:21:37 +02001<?php
2
3class Encryption_test extends CI_TestCase {
4
5 public function set_up()
6 {
Andrey Andreev81e10642014-02-07 01:43:36 +02007 $this->encryption = new Mock_Libraries_Encryption();
8 }
9
Andrey Andreevf30b14a2014-02-20 16:18:37 +020010 // --------------------------------------------------------------------
11
Andrey Andreev81e10642014-02-07 01:43:36 +020012 /**
13 * __construct test
14 *
15 * Covers behavior with $config['encryption_key'] set or not
16 */
17 public function test___construct()
18 {
19 // Assume no configuration from set_up()
20 $this->assertNull($this->encryption->get_key());
21
22 // Try with an empty value
23 $this->ci_set_config('encryption_key');
24 $this->encrypt = new Mock_Libraries_Encryption();
25 $this->assertNull($this->encrypt->get_key());
26
27 $this->ci_set_config('encryption_key', str_repeat("\x0", 16));
28 $this->encrypt = new Mock_Libraries_Encryption();
29 $this->assertEquals(str_repeat("\x0", 16), $this->encrypt->get_key());
Andrey Andreev912831f2014-02-04 17:21:37 +020030 }
31
32 // --------------------------------------------------------------------
33
Andrey Andreev81e10642014-02-07 01:43:36 +020034 /**
35 * hkdf() test
36 *
37 * Applies test vectors described in Appendix A(1-3) RFC5869.
38 * Described vectors 4-7 SHA-1, which we don't support and are
39 * therefore excluded.
40 *
41 * Because our implementation is a single method instead of being
42 * split into hkdf_extract() and hkdf_expand(), we cannot test for
43 * the PRK value. As long as the OKM is correct though, it's fine.
44 *
45 * @link https://tools.ietf.org/rfc/rfc5869.txt
46 */
47 public function test_hkdf()
48 {
49 $vectors = array(
50 // A.1: Basic test case with SHA-256
51 array(
52 'digest' => 'sha256',
53 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b",
54 'salt' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c",
55 'length' => 42,
56 'info' => "\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9",
57 // 'prk' => "\x07\x77\x09\x36\x2c\x2e\x32\xdf\x0d\xdc\x3f\x0d\xc4\x7b\xba\x63\x90\xb6\xc7\x3b\xb5\x0f\x9c\x31\x22\xec\x84\x4a\xd7\xc2\xb3\xe5",
58 'okm' => "\x3c\xb2\x5f\x25\xfa\xac\xd5\x7a\x90\x43\x4f\x64\xd0\x36\x2f\x2a\x2d\x2d\x0a\x90\xcf\x1a\x5a\x4c\x5d\xb0\x2d\x56\xec\xc4\xc5\xbf\x34\x00\x72\x08\xd5\xb8\x87\x18\x58\x65"
59 ),
60 // A.2: Test with SHA-256 and longer inputs/outputs
61 array(
62 'digest' => 'sha256',
63 'ikm' => "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f",
64 'salt' => "\x60\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf",
65 'length' => 82,
66 'info' => "\xb0\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff",
67 // 'prk' => "\x06\xa6\xb8\x8c\x58\x53\x36\x1a\x06\x10\x4c\x9c\xeb\x35\xb4\x5c\xef\x76\x00\x14\x90\x46\x71\x01\x4a\x19\x3f\x40\xc1\x5f\xc2\x44",
68 'okm' => "\xb1\x1e\x39\x8d\xc8\x03\x27\xa1\xc8\xe7\xf7\x8c\x59\x6a\x49\x34\x4f\x01\x2e\xda\x2d\x4e\xfa\xd8\xa0\x50\xcc\x4c\x19\xaf\xa9\x7c\x59\x04\x5a\x99\xca\xc7\x82\x72\x71\xcb\x41\xc6\x5e\x59\x0e\x09\xda\x32\x75\x60\x0c\x2f\x09\xb8\x36\x77\x93\xa9\xac\xa3\xdb\x71\xcc\x30\xc5\x81\x79\xec\x3e\x87\xc1\x4c\x01\xd5\xc1\xf3\x43\x4f\x1d\x87",
69 ),
70 // A.3: Test with SHA-256 and zero-length salt/info
71 array(
72 'digest' => 'sha256',
73 'ikm' => "\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b",
74 'salt' => '',
75 'length' => 42,
76 'info' => '',
77 // 'prk' => "\x19\xef\x24\xa3\x2c\x71\x7b\x16\x7f\x33\xa9\x1d\x6f\x64\x8b\xdf\x96\x59\x67\x76\xaf\xdb\x63\x77\xac\x43\x4c\x1c\x29\x3c\xcb\x04",
78 'okm' => "\x8d\xa4\xe7\x75\xa5\x63\xc1\x8f\x71\x5f\x80\x2a\x06\x3c\x5a\x31\xb8\xa1\x1f\x5c\x5e\xe1\x87\x9e\xc3\x45\x4e\x5f\x3c\x73\x8d\x2d\x9d\x20\x13\x95\xfa\xa4\xb6\x1a\x96\xc8",
79 )
80 );
81
82 foreach ($vectors as $test)
83 {
84 $this->assertEquals(
85 $test['okm'],
86 $this->encryption->hkdf(
87 $test['ikm'],
88 $test['digest'],
89 $test['salt'],
90 $test['length'],
91 $test['info']
92 )
93 );
94 }
95
96 // Test default length, it must match the digest size
Andrey Andreevf5652122017-01-19 15:17:00 +020097 $hkdf_result = $this->encryption->hkdf('foobar', 'sha512');
98 $this->assertEquals(
99 64,
100 defined('MB_OVERLOAD_STRING')
101 ? mb_strlen($hkdf_result, '8bit')
102 : strlen($hkdf_result)
103 );
Andrey Andreev81e10642014-02-07 01:43:36 +0200104
105 // Test maximum length (RFC5869 says that it must be up to 255 times the digest size)
Andrey Andreevf5652122017-01-19 15:17:00 +0200106 $hkdf_result = $this->encryption->hkdf('foobar', 'sha384', NULL, 48 * 255);
107 $this->assertEquals(
108 12240,
109 defined('MB_OVERLOAD_STRING')
110 ? mb_strlen($hkdf_result, '8bit')
111 : strlen($hkdf_result)
112 );
Andrey Andreev81e10642014-02-07 01:43:36 +0200113 $this->assertFalse($this->encryption->hkdf('foobar', 'sha224', NULL, 28 * 255 + 1));
114
115 // CI-specific test for an invalid digest
116 $this->assertFalse($this->encryption->hkdf('fobar', 'sha1'));
117 }
118
119 // --------------------------------------------------------------------
120
121 /**
122 * _get_params() test
Andrey Andreev81e10642014-02-07 01:43:36 +0200123 */
124 public function test__get_params()
125 {
126 $key = str_repeat("\x0", 16);
127
128 // Invalid custom parameters
129 $params = array(
130 // No cipher, mode or key
131 array('cipher' => 'aes-128', 'mode' => 'cbc'),
132 array('cipher' => 'aes-128', 'key' => $key),
133 array('mode' => 'cbc', 'key' => $key),
134 // No HMAC key or not a valid digest
135 array('cipher' => 'aes-128', 'mode' => 'cbc', 'key' => $key),
136 array('cipher' => 'aes-128', 'mode' => 'cbc', 'key' => $key, 'hmac_digest' => 'sha1', 'hmac_key' => $key),
137 // Invalid mode
138 array('cipher' => 'aes-128', 'mode' => 'foo', 'key' => $key, 'hmac_digest' => 'sha256', 'hmac_key' => $key)
139 );
140
141 for ($i = 0, $c = count($params); $i < $c; $i++)
142 {
143 $this->assertFalse($this->encryption->__get_params($params[$i]));
144 }
145
146 // Valid parameters
147 $params = array(
148 'cipher' => 'aes-128',
149 'mode' => 'cbc',
150 'key' => str_repeat("\x0", 16),
151 'hmac_key' => str_repeat("\x0", 16)
152 );
153
Andrey Andreev20d9b0a2017-12-20 19:57:39 +0200154 $this->assertInternalType('array', $this->encryption->__get_params($params));
Andrey Andreev81e10642014-02-07 01:43:36 +0200155
Andrey Andreev81e10642014-02-07 01:43:36 +0200156 $params['base64'] = TRUE;
157 $params['hmac_digest'] = 'sha512';
158
159 // Including all parameters
160 $params = array(
161 'cipher' => 'aes-128',
162 'mode' => 'cbc',
163 'key' => str_repeat("\x0", 16),
Andrey Andreev4b450652014-02-10 06:59:54 +0200164 'raw_data' => TRUE,
Andrey Andreev81e10642014-02-07 01:43:36 +0200165 'hmac_key' => str_repeat("\x0", 16),
166 'hmac_digest' => 'sha256'
167 );
168
169 $output = $this->encryption->__get_params($params);
Andrey Andreev50c9ea12014-11-07 16:57:41 +0200170 unset($output['handle'], $output['cipher'], $params['raw_data'], $params['cipher']);
Andrey Andreev4b450652014-02-10 06:59:54 +0200171 $params['base64'] = FALSE;
Andrey Andreev81e10642014-02-07 01:43:36 +0200172 $this->assertEquals($params, $output);
173
174 // HMAC disabled
175 unset($params['hmac_key'], $params['hmac_digest']);
Andrey Andreev4b450652014-02-10 06:59:54 +0200176 $params['hmac'] = $params['raw_data'] = FALSE;
Andrey Andreev50c9ea12014-11-07 16:57:41 +0200177 $params['cipher'] = 'aes-128';
Andrey Andreev81e10642014-02-07 01:43:36 +0200178 $output = $this->encryption->__get_params($params);
Andrey Andreev50c9ea12014-11-07 16:57:41 +0200179 unset($output['handle'], $output['cipher'], $params['hmac'], $params['raw_data'], $params['cipher']);
Andrey Andreev4b450652014-02-10 06:59:54 +0200180 $params['base64'] = TRUE;
Andrey Andreev81e10642014-02-07 01:43:36 +0200181 $params['hmac_digest'] = $params['hmac_key'] = NULL;
182 $this->assertEquals($params, $output);
183 }
184
185 // --------------------------------------------------------------------
186
187 /**
188 * initialize(), encrypt(), decrypt() test
189 *
190 * Testing the three methods separately is not realistic as they are
191 * designed to work together. A more thorough test for initialize()
192 * though is the OpenSSL/MCrypt compatibility test.
Andrey Andreev1b4e5e12014-02-13 01:14:28 +0200193 *
194 * @depends test_hkdf
195 * @depends test__get_params
Andrey Andreev81e10642014-02-07 01:43:36 +0200196 */
197 public function test_initialize_encrypt_decrypt()
198 {
199 $message = 'This is a plain-text message.';
200 $key = "\xd0\xc9\x08\xc4\xde\x52\x12\x6e\xf8\xcc\xdb\x03\xea\xa0\x3a\x5c";
201
202 // Default state (AES-128/Rijndael-128 in CBC mode)
203 $this->encryption->initialize(array('key' => $key));
204
205 // Was the key properly set?
206 $this->assertEquals($key, $this->encryption->get_key());
207
208 $this->assertEquals($message, $this->encryption->decrypt($this->encryption->encrypt($message)));
209
210 // Try DES in ECB mode, just for the sake of changing stuff
Andrey Andreev50c9ea12014-11-07 16:57:41 +0200211 $this->encryption->initialize(array('cipher' => 'des', 'mode' => 'ecb', 'key' => substr($key, 0, 8)));
Andrey Andreev81e10642014-02-07 01:43:36 +0200212 $this->assertEquals($message, $this->encryption->decrypt($this->encryption->encrypt($message)));
213 }
214
215 // --------------------------------------------------------------------
216
217 /**
218 * encrypt(), decrypt test with custom parameters
Andrey Andreev1b4e5e12014-02-13 01:14:28 +0200219 *
220 * @depends test___get_params
Andrey Andreev81e10642014-02-07 01:43:36 +0200221 */
222 public function test_encrypt_decrypt_custom()
223 {
224 $message = 'Another plain-text message.';
225
226 // A random invalid parameter
227 $this->assertFalse($this->encryption->encrypt($message, array('foo')));
228 $this->assertFalse($this->encryption->decrypt($message, array('foo')));
229
Andrey Andreev1e83d692014-06-19 20:08:59 +0300230 // No HMAC, binary output
Andrey Andreev81e10642014-02-07 01:43:36 +0200231 $params = array(
232 'cipher' => 'tripledes',
233 'mode' => 'cfb',
234 'key' => str_repeat("\x1", 16),
Andrey Andreev81e10642014-02-07 01:43:36 +0200235 'base64' => FALSE,
236 'hmac' => FALSE
237 );
238
239 $ciphertext = $this->encryption->encrypt($message, $params);
Andrey Andreev81e10642014-02-07 01:43:36 +0200240
Andrey Andreev81e10642014-02-07 01:43:36 +0200241 $this->assertEquals($message, $this->encryption->decrypt($ciphertext, $params));
242 }
243
244 // --------------------------------------------------------------------
245
246 /**
247 * _mcrypt_get_handle() test
248 */
249 public function test__mcrypt_get_handle()
250 {
251 if ($this->encryption->drivers['mcrypt'] === FALSE)
252 {
Andrey Andreev1b4e5e12014-02-13 01:14:28 +0200253 return $this->markTestSkipped('Cannot test MCrypt because it is not available.');
Andrey Andreev81e10642014-02-07 01:43:36 +0200254 }
Andrey Andreevc95df512016-08-22 13:19:24 +0300255 elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>='))
256 {
257 return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.');
258 }
Andrey Andreev81e10642014-02-07 01:43:36 +0200259
Andrey Andreev20d9b0a2017-12-20 19:57:39 +0200260 $this->assertInternalType('resource', $this->encryption->__driver_get_handle('mcrypt', 'rijndael-128', 'cbc'));
Andrey Andreev81e10642014-02-07 01:43:36 +0200261 }
262
263 // --------------------------------------------------------------------
264
265 /**
266 * _openssl_get_handle() test
267 */
268 public function test__openssl_mcrypt_get_handle()
269 {
270 if ($this->encryption->drivers['openssl'] === FALSE)
271 {
Andrey Andreev1b4e5e12014-02-13 01:14:28 +0200272 return $this->markTestSkipped('Cannot test OpenSSL because it is not available.');
Andrey Andreev81e10642014-02-07 01:43:36 +0200273 }
274
275 $this->assertEquals('aes-128-cbc', $this->encryption->__driver_get_handle('openssl', 'aes-128', 'cbc'));
276 $this->assertEquals('rc4-40', $this->encryption->__driver_get_handle('openssl', 'rc4-40', 'stream'));
277 }
278
279 // --------------------------------------------------------------------
280
281 /**
282 * OpenSSL/MCrypt portability test
283 *
284 * Amongst the obvious stuff, _cipher_alias() is also tested here.
285 */
Andrey Andreev50ccc382014-02-04 23:30:06 +0200286 public function test_portability()
287 {
288 if ( ! $this->encryption->drivers['mcrypt'] OR ! $this->encryption->drivers['openssl'])
289 {
Andrey Andreev1b4e5e12014-02-13 01:14:28 +0200290 $this->markTestSkipped('Both MCrypt and OpenSSL support are required for portability tests.');
Andrey Andreev50ccc382014-02-04 23:30:06 +0200291 return;
292 }
Andrey Andreevc95df512016-08-22 13:19:24 +0300293 elseif (version_compare(PHP_VERSION, '7.1.0-alpha', '>='))
294 {
295 return $this->markTestSkipped('ext/mcrypt is deprecated since PHP 7.1 and will generate notices here.');
296 }
Andrey Andreev50ccc382014-02-04 23:30:06 +0200297
298 $message = 'This is a message encrypted via MCrypt and decrypted via OpenSSL, or vice-versa.';
299
Andrey Andreevf4017672014-02-05 18:51:15 +0200300 // Format is: <Cipher name>, <Cipher mode>, <Key size>
Andrey Andreev50ccc382014-02-04 23:30:06 +0200301 $portable = array(
Andrey Andreevf4017672014-02-05 18:51:15 +0200302 array('aes-128', 'cbc', 16),
303 array('aes-128', 'cfb', 16),
304 array('aes-128', 'cfb8', 16),
305 array('aes-128', 'ofb', 16),
306 array('aes-128', 'ecb', 16),
307 array('aes-128', 'ctr', 16),
308 array('aes-192', 'cbc', 24),
309 array('aes-192', 'cfb', 24),
310 array('aes-192', 'cfb8', 24),
311 array('aes-192', 'ofb', 24),
312 array('aes-192', 'ecb', 24),
313 array('aes-192', 'ctr', 24),
314 array('aes-256', 'cbc', 32),
315 array('aes-256', 'cfb', 32),
316 array('aes-256', 'cfb8', 32),
317 array('aes-256', 'ofb', 32),
318 array('aes-256', 'ecb', 32),
319 array('aes-256', 'ctr', 32),
320 array('des', 'cbc', 7),
321 array('des', 'cfb', 7),
322 array('des', 'cfb8', 7),
323 array('des', 'ofb', 7),
324 array('des', 'ecb', 7),
325 array('tripledes', 'cbc', 7),
326 array('tripledes', 'cfb', 7),
327 array('tripledes', 'cfb8', 7),
328 array('tripledes', 'ofb', 7),
329 array('tripledes', 'cbc', 14),
330 array('tripledes', 'cfb', 14),
331 array('tripledes', 'cfb8', 14),
332 array('tripledes', 'ofb', 14),
333 array('tripledes', 'cbc', 21),
334 array('tripledes', 'cfb', 21),
335 array('tripledes', 'cfb8', 21),
336 array('tripledes', 'ofb', 21),
337 array('blowfish', 'cbc', 16),
338 array('blowfish', 'cfb', 16),
339 array('blowfish', 'ofb', 16),
340 array('blowfish', 'ecb', 16),
341 array('blowfish', 'cbc', 56),
342 array('blowfish', 'cfb', 56),
343 array('blowfish', 'ofb', 56),
344 array('blowfish', 'ecb', 56),
Andrey Andreev18767e32014-03-04 22:21:35 +0200345 array('cast5', 'cbc', 11),
346 array('cast5', 'cfb', 11),
347 array('cast5', 'ofb', 11),
348 array('cast5', 'ecb', 11),
Andrey Andreeve8088d62014-02-06 05:01:48 +0200349 array('cast5', 'cbc', 16),
350 array('cast5', 'cfb', 16),
351 array('cast5', 'ofb', 16),
352 array('cast5', 'ecb', 16),
353 array('rc4', 'stream', 5),
354 array('rc4', 'stream', 8),
355 array('rc4', 'stream', 16),
356 array('rc4', 'stream', 32),
357 array('rc4', 'stream', 64),
358 array('rc4', 'stream', 128),
359 array('rc4', 'stream', 256)
Andrey Andreev50ccc382014-02-04 23:30:06 +0200360 );
361 $driver_index = array('mcrypt', 'openssl');
362
363 foreach ($portable as &$test)
364 {
365 // Add some randomness to the selected driver
366 $driver = mt_rand(0,1);
367 $params = array(
Andrey Andreev8d33a9a2014-02-05 23:11:23 +0200368 'driver' => $driver_index[$driver],
Andrey Andreevf4017672014-02-05 18:51:15 +0200369 'cipher' => $test[0],
370 'mode' => $test[1],
Andrey Andreev50ccc382014-02-04 23:30:06 +0200371 'key' => openssl_random_pseudo_bytes($test[2])
372 );
373
Andrey Andreev8d33a9a2014-02-05 23:11:23 +0200374 $this->encryption->initialize($params);
375 $ciphertext = $this->encryption->encrypt($message);
Andrey Andreev50ccc382014-02-04 23:30:06 +0200376
377 $driver = (int) ! $driver;
Andrey Andreev8d33a9a2014-02-05 23:11:23 +0200378 $params['driver'] = $driver_index[$driver];
Andrey Andreev50ccc382014-02-04 23:30:06 +0200379
Andrey Andreev8d33a9a2014-02-05 23:11:23 +0200380 $this->encryption->initialize($params);
381 $this->assertEquals($message, $this->encryption->decrypt($ciphertext));
Andrey Andreev50ccc382014-02-04 23:30:06 +0200382 }
383 }
384
385 // --------------------------------------------------------------------
386
Andrey Andreev81e10642014-02-07 01:43:36 +0200387 /**
388 * __get() test
389 */
390 public function test_magic_get()
Andrey Andreev912831f2014-02-04 17:21:37 +0200391 {
Andrey Andreev81e10642014-02-07 01:43:36 +0200392 $this->assertNull($this->encryption->foo);
393 $this->assertEquals(array('mcrypt', 'openssl'), array_keys($this->encryption->drivers));
Andrey Andreev912831f2014-02-04 17:21:37 +0200394
Andrey Andreev81e10642014-02-07 01:43:36 +0200395 // 'stream' mode is translated into an empty string for OpenSSL
396 $this->encryption->initialize(array('cipher' => 'rc4', 'mode' => 'stream'));
397 $this->assertEquals('stream', $this->encryption->mode);
Andrey Andreev912831f2014-02-04 17:21:37 +0200398 }
399
Andrey Andreevc95df512016-08-22 13:19:24 +0300400}