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