1<?php 2 3namespace Defuse\Crypto; 4 5use Defuse\Crypto\Exception as Ex; 6 7/* 8 * We're using static class inheritance to get access to protected methods 9 * inside Crypto. To make it easy to know where the method we're calling can be 10 * found, within this file, prefix calls with `Crypto::` or `RuntimeTests::`, 11 * and don't use `self::`. 12 */ 13 14class RuntimeTests extends Crypto 15{ 16 /** 17 * Runs the runtime tests. 18 * 19 * @throws Ex\EnvironmentIsBrokenException 20 */ 21 public static function runtimeTest() 22 { 23 // 0: Tests haven't been run yet. 24 // 1: Tests have passed. 25 // 2: Tests are running right now. 26 // 3: Tests have failed. 27 static $test_state = 0; 28 29 if ($test_state === 1 || $test_state === 2) { 30 return; 31 } 32 33 if ($test_state === 3) { 34 /* If an intermittent problem caused a test to fail previously, we 35 * want that to be indicated to the user with every call to this 36 * library. This way, if the user first does something they really 37 * don't care about, and just ignores all exceptions, they won't get 38 * screwed when they then start to use the library for something 39 * they do care about. */ 40 throw new Ex\EnvironmentIsBrokenException('Tests failed previously.'); 41 } 42 43 try { 44 $test_state = 2; 45 46 Core::ensureFunctionExists('openssl_get_cipher_methods'); 47 if (\in_array(Core::CIPHER_METHOD, \openssl_get_cipher_methods()) === false) { 48 throw new Ex\EnvironmentIsBrokenException( 49 'Cipher method not supported. This is normally caused by an outdated ' . 50 'version of OpenSSL (and/or OpenSSL compiled for FIPS compliance). ' . 51 'Please upgrade to a newer version of OpenSSL that supports ' . 52 Core::CIPHER_METHOD . ' to use this library.' 53 ); 54 } 55 56 RuntimeTests::AESTestVector(); 57 RuntimeTests::HMACTestVector(); 58 RuntimeTests::HKDFTestVector(); 59 60 RuntimeTests::testEncryptDecrypt(); 61 if (Core::ourStrlen(Key::createNewRandomKey()->getRawBytes()) != Core::KEY_BYTE_SIZE) { 62 throw new Ex\EnvironmentIsBrokenException(); 63 } 64 65 if (Core::ENCRYPTION_INFO_STRING == Core::AUTHENTICATION_INFO_STRING) { 66 throw new Ex\EnvironmentIsBrokenException(); 67 } 68 } catch (Ex\EnvironmentIsBrokenException $ex) { 69 // Do this, otherwise it will stay in the "tests are running" state. 70 $test_state = 3; 71 throw $ex; 72 } 73 74 // Change this to '0' make the tests always re-run (for benchmarking). 75 $test_state = 1; 76 } 77 78 /** 79 * High-level tests of Crypto operations. 80 * 81 * @throws Ex\EnvironmentIsBrokenException 82 */ 83 private static function testEncryptDecrypt() 84 { 85 $key = Key::createNewRandomKey(); 86 $data = "EnCrYpT EvErYThInG\x00\x00"; 87 88 // Make sure encrypting then decrypting doesn't change the message. 89 $ciphertext = Crypto::encrypt($data, $key, true); 90 try { 91 $decrypted = Crypto::decrypt($ciphertext, $key, true); 92 } catch (Ex\WrongKeyOrModifiedCiphertextException $ex) { 93 // It's important to catch this and change it into a 94 // Ex\EnvironmentIsBrokenException, otherwise a test failure could trick 95 // the user into thinking it's just an invalid ciphertext! 96 throw new Ex\EnvironmentIsBrokenException(); 97 } 98 if ($decrypted !== $data) { 99 throw new Ex\EnvironmentIsBrokenException(); 100 } 101 102 // Modifying the ciphertext: Appending a string. 103 try { 104 Crypto::decrypt($ciphertext . 'a', $key, true); 105 throw new Ex\EnvironmentIsBrokenException(); 106 } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ 107 } 108 109 // Modifying the ciphertext: Changing an HMAC byte. 110 $indices_to_change = [ 111 0, // The header. 112 Core::HEADER_VERSION_SIZE + 1, // the salt 113 Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + 1, // the IV 114 Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE + Core::BLOCK_BYTE_SIZE + 1, // the ciphertext 115 ]; 116 117 foreach ($indices_to_change as $index) { 118 try { 119 $ciphertext[$index] = \chr((\ord($ciphertext[$index]) + 1) % 256); 120 Crypto::decrypt($ciphertext, $key, true); 121 throw new Ex\EnvironmentIsBrokenException(); 122 } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ 123 } 124 } 125 126 // Decrypting with the wrong key. 127 $key = Key::createNewRandomKey(); 128 $data = 'abcdef'; 129 $ciphertext = Crypto::encrypt($data, $key, true); 130 $wrong_key = Key::createNewRandomKey(); 131 try { 132 Crypto::decrypt($ciphertext, $wrong_key, true); 133 throw new Ex\EnvironmentIsBrokenException(); 134 } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ 135 } 136 137 // Ciphertext too small. 138 $key = Key::createNewRandomKey(); 139 $ciphertext = \str_repeat('A', Core::MINIMUM_CIPHERTEXT_SIZE - 1); 140 try { 141 Crypto::decrypt($ciphertext, $key, true); 142 throw new Ex\EnvironmentIsBrokenException(); 143 } catch (Ex\WrongKeyOrModifiedCiphertextException $e) { /* expected */ 144 } 145 } 146 147 /** 148 * Test HKDF against test vectors. 149 * 150 * @throws Ex\EnvironmentIsBrokenException 151 */ 152 private static function HKDFTestVector() 153 { 154 // HKDF test vectors from RFC 5869 155 156 // Test Case 1 157 $ikm = \str_repeat("\x0b", 22); 158 $salt = Encoding::hexToBin('000102030405060708090a0b0c'); 159 $info = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9'); 160 $length = 42; 161 $okm = Encoding::hexToBin( 162 '3cb25f25faacd57a90434f64d0362f2a' . 163 '2d2d0a90cf1a5a4c5db02d56ecc4c5bf' . 164 '34007208d5b887185865' 165 ); 166 $computed_okm = Core::HKDF('sha256', $ikm, $length, $info, $salt); 167 if ($computed_okm !== $okm) { 168 throw new Ex\EnvironmentIsBrokenException(); 169 } 170 171 // Test Case 7 172 $ikm = \str_repeat("\x0c", 22); 173 $length = 42; 174 $okm = Encoding::hexToBin( 175 '2c91117204d745f3500d636a62f64f0a' . 176 'b3bae548aa53d423b0d1f27ebba6f5e5' . 177 '673a081d70cce7acfc48' 178 ); 179 $computed_okm = Core::HKDF('sha1', $ikm, $length, '', null); 180 if ($computed_okm !== $okm) { 181 throw new Ex\EnvironmentIsBrokenException(); 182 } 183 } 184 185 /** 186 * Test HMAC against test vectors. 187 * 188 * @throws Ex\EnvironmentIsBrokenException 189 */ 190 private static function HMACTestVector() 191 { 192 // HMAC test vector From RFC 4231 (Test Case 1) 193 $key = \str_repeat("\x0b", 20); 194 $data = 'Hi There'; 195 $correct = 'b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7'; 196 if (\hash_hmac(Core::HASH_FUNCTION_NAME, $data, $key) !== $correct) { 197 throw new Ex\EnvironmentIsBrokenException(); 198 } 199 } 200 201 /** 202 * Test AES against test vectors. 203 * 204 * @throws Ex\EnvironmentIsBrokenException 205 */ 206 private static function AESTestVector() 207 { 208 // AES CTR mode test vector from NIST SP 800-38A 209 $key = Encoding::hexToBin( 210 '603deb1015ca71be2b73aef0857d7781' . 211 '1f352c073b6108d72d9810a30914dff4' 212 ); 213 $iv = Encoding::hexToBin('f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff'); 214 $plaintext = Encoding::hexToBin( 215 '6bc1bee22e409f96e93d7e117393172a' . 216 'ae2d8a571e03ac9c9eb76fac45af8e51' . 217 '30c81c46a35ce411e5fbc1191a0a52ef' . 218 'f69f2445df4f9b17ad2b417be66c3710' 219 ); 220 $ciphertext = Encoding::hexToBin( 221 '601ec313775789a5b7a7f504bbf3d228' . 222 'f443e3ca4d62b59aca84e990cacaf5c5' . 223 '2b0930daa23de94ce87017ba2d84988d' . 224 'dfc9c58db67aada613c2dd08457941a6' 225 ); 226 227 $computed_ciphertext = Crypto::plainEncrypt($plaintext, $key, $iv); 228 if ($computed_ciphertext !== $ciphertext) { 229 echo \str_repeat("\n", 30); 230 echo \bin2hex($computed_ciphertext); 231 echo "\n---\n"; 232 echo \bin2hex($ciphertext); 233 echo \str_repeat("\n", 30); 234 throw new Ex\EnvironmentIsBrokenException(); 235 } 236 237 $computed_plaintext = Crypto::plainDecrypt($ciphertext, $key, $iv, Core::CIPHER_METHOD); 238 if ($computed_plaintext !== $plaintext) { 239 throw new Ex\EnvironmentIsBrokenException(); 240 } 241 } 242} 243