1<?php
2
3namespace Defuse\Crypto;
4
5use Defuse\Crypto\Exception as Ex;
6
7class Crypto
8{
9    /**
10     * Encrypts a string with a Key.
11     *
12     * @param string $plaintext
13     * @param Key    $key
14     * @param bool   $raw_binary
15     *
16     * @throws Ex\EnvironmentIsBrokenException
17     * @throws \TypeError
18     *
19     * @return string
20     */
21    public static function encrypt($plaintext, $key, $raw_binary = false)
22    {
23        if (!\is_string($plaintext)) {
24            throw new \TypeError(
25                'String expected for argument 1. ' . \ucfirst(\gettype($plaintext)) . ' given instead.'
26            );
27        }
28        if (!($key instanceof Key)) {
29            throw new \TypeError(
30                'Key expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.'
31            );
32        }
33        if (!\is_bool($raw_binary)) {
34            throw new \TypeError(
35                'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
36            );
37        }
38        return self::encryptInternal(
39            $plaintext,
40            KeyOrPassword::createFromKey($key),
41            $raw_binary
42        );
43    }
44
45    /**
46     * Encrypts a string with a password, using a slow key derivation function
47     * to make password cracking more expensive.
48     *
49     * @param string $plaintext
50     * @param string $password
51     * @param bool   $raw_binary
52     *
53     * @throws Ex\EnvironmentIsBrokenException
54     * @throws \TypeError
55     *
56     * @return string
57     */
58    public static function encryptWithPassword($plaintext, $password, $raw_binary = false)
59    {
60        if (!\is_string($plaintext)) {
61            throw new \TypeError(
62                'String expected for argument 1. ' . \ucfirst(\gettype($plaintext)) . ' given instead.'
63            );
64        }
65        if (!\is_string($password)) {
66            throw new \TypeError(
67                'String expected for argument 2. ' . \ucfirst(\gettype($password)) . ' given instead.'
68            );
69        }
70        if (!\is_bool($raw_binary)) {
71            throw new \TypeError(
72                'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
73            );
74        }
75        return self::encryptInternal(
76            $plaintext,
77            KeyOrPassword::createFromPassword($password),
78            $raw_binary
79        );
80    }
81
82    /**
83     * Decrypts a ciphertext to a string with a Key.
84     *
85     * @param string $ciphertext
86     * @param Key    $key
87     * @param bool   $raw_binary
88     *
89     * @throws \TypeError
90     * @throws Ex\EnvironmentIsBrokenException
91     * @throws Ex\WrongKeyOrModifiedCiphertextException
92     *
93     * @return string
94     */
95    public static function decrypt($ciphertext, $key, $raw_binary = false)
96    {
97        if (!\is_string($ciphertext)) {
98            throw new \TypeError(
99                'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.'
100            );
101        }
102        if (!($key instanceof Key)) {
103            throw new \TypeError(
104                'Key expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.'
105            );
106        }
107        if (!\is_bool($raw_binary)) {
108            throw new \TypeError(
109                'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
110            );
111        }
112        return self::decryptInternal(
113            $ciphertext,
114            KeyOrPassword::createFromKey($key),
115            $raw_binary
116        );
117    }
118
119    /**
120     * Decrypts a ciphertext to a string with a password, using a slow key
121     * derivation function to make password cracking more expensive.
122     *
123     * @param string $ciphertext
124     * @param string $password
125     * @param bool   $raw_binary
126     *
127     * @throws Ex\EnvironmentIsBrokenException
128     * @throws Ex\WrongKeyOrModifiedCiphertextException
129     * @throws \TypeError
130     *
131     * @return string
132     */
133    public static function decryptWithPassword($ciphertext, $password, $raw_binary = false)
134    {
135        if (!\is_string($ciphertext)) {
136            throw new \TypeError(
137                'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.'
138            );
139        }
140        if (!\is_string($password)) {
141            throw new \TypeError(
142                'String expected for argument 2. ' . \ucfirst(\gettype($password)) . ' given instead.'
143            );
144        }
145        if (!\is_bool($raw_binary)) {
146            throw new \TypeError(
147                'Boolean expected for argument 3. ' . \ucfirst(\gettype($raw_binary)) . ' given instead.'
148            );
149        }
150        return self::decryptInternal(
151            $ciphertext,
152            KeyOrPassword::createFromPassword($password),
153            $raw_binary
154        );
155    }
156
157    /**
158     * Decrypts a legacy ciphertext produced by version 1 of this library.
159     *
160     * @param string $ciphertext
161     * @param string $key
162     *
163     * @throws Ex\EnvironmentIsBrokenException
164     * @throws Ex\WrongKeyOrModifiedCiphertextException
165     * @throws \TypeError
166     *
167     * @return string
168     */
169    public static function legacyDecrypt($ciphertext, $key)
170    {
171        if (!\is_string($ciphertext)) {
172            throw new \TypeError(
173                'String expected for argument 1. ' . \ucfirst(\gettype($ciphertext)) . ' given instead.'
174            );
175        }
176        if (!\is_string($key)) {
177            throw new \TypeError(
178                'String expected for argument 2. ' . \ucfirst(\gettype($key)) . ' given instead.'
179            );
180        }
181
182        RuntimeTests::runtimeTest();
183
184        // Extract the HMAC from the front of the ciphertext.
185        if (Core::ourStrlen($ciphertext) <= Core::LEGACY_MAC_BYTE_SIZE) {
186            throw new Ex\WrongKeyOrModifiedCiphertextException(
187                'Ciphertext is too short.'
188            );
189        }
190        /**
191         * @var string
192         */
193        $hmac = Core::ourSubstr($ciphertext, 0, Core::LEGACY_MAC_BYTE_SIZE);
194        Core::ensureTrue(\is_string($hmac));
195        /**
196         * @var string
197         */
198        $messageCiphertext = Core::ourSubstr($ciphertext, Core::LEGACY_MAC_BYTE_SIZE);
199        Core::ensureTrue(\is_string($messageCiphertext));
200
201        // Regenerate the same authentication sub-key.
202        $akey = Core::HKDF(
203            Core::LEGACY_HASH_FUNCTION_NAME,
204            $key,
205            Core::LEGACY_KEY_BYTE_SIZE,
206            Core::LEGACY_AUTHENTICATION_INFO_STRING,
207            null
208        );
209
210        if (self::verifyHMAC($hmac, $messageCiphertext, $akey)) {
211            // Regenerate the same encryption sub-key.
212            $ekey = Core::HKDF(
213                Core::LEGACY_HASH_FUNCTION_NAME,
214                $key,
215                Core::LEGACY_KEY_BYTE_SIZE,
216                Core::LEGACY_ENCRYPTION_INFO_STRING,
217                null
218            );
219
220            // Extract the IV from the ciphertext.
221            if (Core::ourStrlen($messageCiphertext) <= Core::LEGACY_BLOCK_BYTE_SIZE) {
222                throw new Ex\WrongKeyOrModifiedCiphertextException(
223                    'Ciphertext is too short.'
224                );
225            }
226            /**
227             * @var string
228             */
229            $iv = Core::ourSubstr($messageCiphertext, 0, Core::LEGACY_BLOCK_BYTE_SIZE);
230            Core::ensureTrue(\is_string($iv));
231
232            /**
233             * @var string
234             */
235            $actualCiphertext = Core::ourSubstr($messageCiphertext, Core::LEGACY_BLOCK_BYTE_SIZE);
236            Core::ensureTrue(\is_string($actualCiphertext));
237
238            // Do the decryption.
239            $plaintext = self::plainDecrypt($actualCiphertext, $ekey, $iv, Core::LEGACY_CIPHER_METHOD);
240            return $plaintext;
241        } else {
242            throw new Ex\WrongKeyOrModifiedCiphertextException(
243                'Integrity check failed.'
244            );
245        }
246    }
247
248    /**
249     * Encrypts a string with either a key or a password.
250     *
251     * @param string        $plaintext
252     * @param KeyOrPassword $secret
253     * @param bool          $raw_binary
254     *
255     * @return string
256     */
257    private static function encryptInternal($plaintext, KeyOrPassword $secret, $raw_binary)
258    {
259        RuntimeTests::runtimeTest();
260
261        $salt = Core::secureRandom(Core::SALT_BYTE_SIZE);
262        $keys = $secret->deriveKeys($salt);
263        $ekey = $keys->getEncryptionKey();
264        $akey = $keys->getAuthenticationKey();
265        $iv     = Core::secureRandom(Core::BLOCK_BYTE_SIZE);
266
267        $ciphertext = Core::CURRENT_VERSION . $salt . $iv . self::plainEncrypt($plaintext, $ekey, $iv);
268        $auth       = \hash_hmac(Core::HASH_FUNCTION_NAME, $ciphertext, $akey, true);
269        $ciphertext = $ciphertext . $auth;
270
271        if ($raw_binary) {
272            return $ciphertext;
273        }
274        return Encoding::binToHex($ciphertext);
275    }
276
277    /**
278     * Decrypts a ciphertext to a string with either a key or a password.
279     *
280     * @param string        $ciphertext
281     * @param KeyOrPassword $secret
282     * @param bool          $raw_binary
283     *
284     * @throws Ex\EnvironmentIsBrokenException
285     * @throws Ex\WrongKeyOrModifiedCiphertextException
286     *
287     * @return string
288     */
289    private static function decryptInternal($ciphertext, KeyOrPassword $secret, $raw_binary)
290    {
291        RuntimeTests::runtimeTest();
292
293        if (! $raw_binary) {
294            try {
295                $ciphertext = Encoding::hexToBin($ciphertext);
296            } catch (Ex\BadFormatException $ex) {
297                throw new Ex\WrongKeyOrModifiedCiphertextException(
298                    'Ciphertext has invalid hex encoding.'
299                );
300            }
301        }
302
303        if (Core::ourStrlen($ciphertext) < Core::MINIMUM_CIPHERTEXT_SIZE) {
304            throw new Ex\WrongKeyOrModifiedCiphertextException(
305                'Ciphertext is too short.'
306            );
307        }
308
309        // Get and check the version header.
310        /** @var string $header */
311        $header = Core::ourSubstr($ciphertext, 0, Core::HEADER_VERSION_SIZE);
312        if ($header !== Core::CURRENT_VERSION) {
313            throw new Ex\WrongKeyOrModifiedCiphertextException(
314                'Bad version header.'
315            );
316        }
317
318        // Get the salt.
319        /** @var string $salt */
320        $salt = Core::ourSubstr(
321            $ciphertext,
322            Core::HEADER_VERSION_SIZE,
323            Core::SALT_BYTE_SIZE
324        );
325        Core::ensureTrue(\is_string($salt));
326
327        // Get the IV.
328        /** @var string $iv */
329        $iv = Core::ourSubstr(
330            $ciphertext,
331            Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE,
332            Core::BLOCK_BYTE_SIZE
333        );
334        Core::ensureTrue(\is_string($iv));
335
336        // Get the HMAC.
337        /** @var string $hmac */
338        $hmac = Core::ourSubstr(
339            $ciphertext,
340            Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE,
341            Core::MAC_BYTE_SIZE
342        );
343        Core::ensureTrue(\is_string($hmac));
344
345        // Get the actual encrypted ciphertext.
346        /** @var string $encrypted */
347        $encrypted = Core::ourSubstr(
348            $ciphertext,
349            Core::HEADER_VERSION_SIZE + Core::SALT_BYTE_SIZE +
350                Core::BLOCK_BYTE_SIZE,
351            Core::ourStrlen($ciphertext) - Core::MAC_BYTE_SIZE - Core::SALT_BYTE_SIZE -
352                Core::BLOCK_BYTE_SIZE - Core::HEADER_VERSION_SIZE
353        );
354        Core::ensureTrue(\is_string($encrypted));
355
356        // Derive the separate encryption and authentication keys from the key
357        // or password, whichever it is.
358        $keys = $secret->deriveKeys($salt);
359
360        if (self::verifyHMAC($hmac, $header . $salt . $iv . $encrypted, $keys->getAuthenticationKey())) {
361            $plaintext = self::plainDecrypt($encrypted, $keys->getEncryptionKey(), $iv, Core::CIPHER_METHOD);
362            return $plaintext;
363        } else {
364            throw new Ex\WrongKeyOrModifiedCiphertextException(
365                'Integrity check failed.'
366            );
367        }
368    }
369
370    /**
371     * Raw unauthenticated encryption (insecure on its own).
372     *
373     * @param string $plaintext
374     * @param string $key
375     * @param string $iv
376     *
377     * @throws Ex\EnvironmentIsBrokenException
378     *
379     * @return string
380     */
381    protected static function plainEncrypt($plaintext, $key, $iv)
382    {
383        Core::ensureConstantExists('OPENSSL_RAW_DATA');
384        Core::ensureFunctionExists('openssl_encrypt');
385        /** @var string $ciphertext */
386        $ciphertext = \openssl_encrypt(
387            $plaintext,
388            Core::CIPHER_METHOD,
389            $key,
390            OPENSSL_RAW_DATA,
391            $iv
392        );
393
394        Core::ensureTrue(\is_string($ciphertext), 'openssl_encrypt() failed');
395
396        return $ciphertext;
397    }
398
399    /**
400     * Raw unauthenticated decryption (insecure on its own).
401     *
402     * @param string $ciphertext
403     * @param string $key
404     * @param string $iv
405     * @param string $cipherMethod
406     *
407     * @throws Ex\EnvironmentIsBrokenException
408     *
409     * @return string
410     */
411    protected static function plainDecrypt($ciphertext, $key, $iv, $cipherMethod)
412    {
413        Core::ensureConstantExists('OPENSSL_RAW_DATA');
414        Core::ensureFunctionExists('openssl_decrypt');
415
416        /** @var string $plaintext */
417        $plaintext = \openssl_decrypt(
418            $ciphertext,
419            $cipherMethod,
420            $key,
421            OPENSSL_RAW_DATA,
422            $iv
423        );
424        Core::ensureTrue(\is_string($plaintext), 'openssl_decrypt() failed.');
425
426        return $plaintext;
427    }
428
429    /**
430     * Verifies an HMAC without leaking information through side-channels.
431     *
432     * @param string $expected_hmac
433     * @param string $message
434     * @param string $key
435     *
436     * @throws Ex\EnvironmentIsBrokenException
437     *
438     * @return bool
439     */
440    protected static function verifyHMAC($expected_hmac, $message, $key)
441    {
442        $message_hmac = \hash_hmac(Core::HASH_FUNCTION_NAME, $message, $key, true);
443        return Core::hashEquals($message_hmac, $expected_hmac);
444    }
445}
446