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