1<?php
2
3/**
4 * @see       https://github.com/laminas/laminas-crypt for the canonical source repository
5 * @copyright https://github.com/laminas/laminas-crypt/blob/master/COPYRIGHT.md
6 * @license   https://github.com/laminas/laminas-crypt/blob/master/LICENSE.md New BSD License
7 */
8
9namespace Laminas\Crypt\Symmetric;
10
11use Interop\Container\ContainerInterface;
12use Laminas\Stdlib\ArrayUtils;
13use Traversable;
14
15use function array_key_exists;
16use function array_keys;
17use function class_exists;
18use function extension_loaded;
19use function get_class;
20use function gettype;
21use function implode;
22use function in_array;
23use function is_array;
24use function is_object;
25use function is_string;
26use function is_subclass_of;
27use function mb_strlen;
28use function mb_substr;
29use function mcrypt_decrypt;
30use function mcrypt_encrypt;
31use function mcrypt_get_block_size;
32use function mcrypt_get_iv_size;
33use function mcrypt_get_key_size;
34use function mcrypt_module_get_supported_key_sizes;
35use function sprintf;
36use function strtolower;
37use function trigger_error;
38
39use const PHP_VERSION_ID;
40
41/**
42 * Symmetric encryption using the Mcrypt extension
43 *
44 * NOTE: DO NOT USE only this class to encrypt data.
45 * This class doesn't provide authentication and integrity check over the data.
46 * PLEASE USE Laminas\Crypt\BlockCipher instead!
47 */
48class Mcrypt implements SymmetricInterface
49{
50    const DEFAULT_PADDING = 'pkcs7';
51
52    /**
53     * Key
54     *
55     * @var string
56     */
57    protected $key;
58
59    /**
60     * IV
61     *
62     * @var string
63     */
64    protected $iv;
65
66    /**
67     * Encryption algorithm
68     *
69     * @var string
70     */
71    protected $algo = 'aes';
72
73    /**
74     * Encryption mode
75     *
76     * @var string
77     */
78    protected $mode = 'cbc';
79
80    /**
81     * Padding
82     *
83     * @var Padding\PaddingInterface
84     */
85    protected $padding;
86
87    /**
88     * Padding plugins
89     *
90     * @var Interop\Container\ContainerInterface
91     */
92    protected static $paddingPlugins = null;
93
94    /**
95     * Supported cipher algorithms
96     *
97     * @var array
98     */
99    protected $supportedAlgos = [
100        'aes'          => 'rijndael-128',
101        'blowfish'     => 'blowfish',
102        'des'          => 'des',
103        '3des'         => 'tripledes',
104        'tripledes'    => 'tripledes',
105        'cast-128'     => 'cast-128',
106        'cast-256'     => 'cast-256',
107        'rijndael-128' => 'rijndael-128',
108        'rijndael-192' => 'rijndael-192',
109        'rijndael-256' => 'rijndael-256',
110        'saferplus'    => 'saferplus',
111        'serpent'      => 'serpent',
112        'twofish'      => 'twofish'
113    ];
114
115    /**
116     * Supported encryption modes
117     *
118     * @var array
119     */
120    protected $supportedModes = [
121        'cbc'  => 'cbc',
122        'cfb'  => 'cfb',
123        'ctr'  => 'ctr',
124        'ofb'  => 'ofb',
125        'nofb' => 'nofb',
126        'ncfb' => 'ncfb'
127    ];
128
129    /**
130     * Constructor
131     *
132     * @param  array|Traversable                  $options
133     * @throws Exception\RuntimeException
134     * @throws Exception\InvalidArgumentException
135     */
136    public function __construct($options = [])
137    {
138        if (PHP_VERSION_ID >= 70100) {
139            trigger_error(
140                'The Mcrypt extension is deprecated from PHP 7.1+. '
141                . 'We suggest to use Laminas\Crypt\Symmetric\Openssl.',
142                E_USER_DEPRECATED
143            );
144        }
145        if (! extension_loaded('mcrypt')) {
146            throw new Exception\RuntimeException(sprintf(
147                'You cannot use %s without the Mcrypt extension',
148                __CLASS__
149            ));
150        }
151        $this->setOptions($options);
152        $this->setDefaultOptions($options);
153    }
154
155    /**
156     * Set default options
157     *
158     * @param  array $options
159     * @return void
160     */
161    public function setOptions($options)
162    {
163        if (! empty($options)) {
164            if ($options instanceof Traversable) {
165                $options = ArrayUtils::iteratorToArray($options);
166            } elseif (! is_array($options)) {
167                throw new Exception\InvalidArgumentException(
168                    'The options parameter must be an array or a Traversable'
169                );
170            }
171            foreach ($options as $key => $value) {
172                switch (strtolower($key)) {
173                    case 'algo':
174                    case 'algorithm':
175                        $this->setAlgorithm($value);
176                        break;
177                    case 'mode':
178                        $this->setMode($value);
179                        break;
180                    case 'key':
181                        $this->setKey($value);
182                        break;
183                    case 'iv':
184                    case 'salt':
185                        $this->setSalt($value);
186                        break;
187                    case 'padding':
188                        $plugins       = static::getPaddingPluginManager();
189                        $padding       = $plugins->get($value);
190                        $this->padding = $padding;
191                        break;
192                }
193            }
194        }
195    }
196
197    /**
198     * Set default options
199     *
200     * @param  array $options
201     * @return void
202     */
203    protected function setDefaultOptions($options = [])
204    {
205        if (! isset($options['padding'])) {
206            $plugins       = static::getPaddingPluginManager();
207            $padding       = $plugins->get(self::DEFAULT_PADDING);
208            $this->padding = $padding;
209        }
210    }
211
212    /**
213     * Returns the padding plugin manager.  If it doesn't exist it's created.
214     *
215     * @return ContainerInterface
216     */
217    public static function getPaddingPluginManager()
218    {
219        if (static::$paddingPlugins === null) {
220            self::setPaddingPluginManager(new PaddingPluginManager());
221        }
222
223        return static::$paddingPlugins;
224    }
225
226    /**
227     * Set the padding plugin manager
228     *
229     * @param  string|ContainerInterface $plugins
230     * @throws Exception\InvalidArgumentException
231     * @return void
232     */
233    public static function setPaddingPluginManager($plugins)
234    {
235        if (is_string($plugins)) {
236            if (! class_exists($plugins) || ! is_subclass_of($plugins, ContainerInterface::class)) {
237                throw new Exception\InvalidArgumentException(sprintf(
238                    'Unable to locate padding plugin manager via class "%s"; '
239                    . 'class does not exist or does not implement ContainerInterface',
240                    $plugins
241                ));
242            }
243            $plugins = new $plugins();
244        }
245        if (! $plugins instanceof ContainerInterface) {
246            throw new Exception\InvalidArgumentException(sprintf(
247                'Padding plugins must implements Interop\Container\ContainerInterface; received "%s"',
248                is_object($plugins) ? get_class($plugins) : gettype($plugins)
249            ));
250        }
251        static::$paddingPlugins = $plugins;
252    }
253
254    /**
255     * Get the maximum key size for the selected cipher and mode of operation
256     *
257     * @return int
258     */
259    public function getKeySize()
260    {
261        return mcrypt_get_key_size($this->supportedAlgos[$this->algo], $this->supportedModes[$this->mode]);
262    }
263
264    /**
265     * Set the encryption key
266     * If the key is longer than maximum supported, it will be truncated by getKey().
267     *
268     * @param  string $key
269     * @return Mcrypt Provides a fluent interface
270     * @throws Exception\InvalidArgumentException
271     */
272    public function setKey($key)
273    {
274        $keyLen = mb_strlen($key, '8bit');
275
276        if (! $keyLen) {
277            throw new Exception\InvalidArgumentException('The key cannot be empty');
278        }
279        $keySizes = mcrypt_module_get_supported_key_sizes($this->supportedAlgos[$this->algo]);
280        $maxKey = $this->getKeySize();
281
282        /*
283         * blowfish has $keySizes empty, meaning it can have arbitrary key length.
284         * the others are more picky.
285         */
286        if (! empty($keySizes) && $keyLen < $maxKey) {
287            if (! in_array($keyLen, $keySizes)) {
288                throw new Exception\InvalidArgumentException(sprintf(
289                    'The size of the key must be %s bytes or longer',
290                    implode(', ', $keySizes)
291                ));
292            }
293        }
294        $this->key = $key;
295
296        return $this;
297    }
298
299    /**
300     * Get the encryption key
301     *
302     * @return string
303     */
304    public function getKey()
305    {
306        if (empty($this->key)) {
307            return;
308        }
309        return mb_substr($this->key, 0, $this->getKeySize(), '8bit');
310    }
311
312    /**
313     * Set the encryption algorithm (cipher)
314     *
315     * @param  string $algo
316     * @return Mcrypt Provides a fluent interface
317     * @throws Exception\InvalidArgumentException
318     */
319    public function setAlgorithm($algo)
320    {
321        if (! array_key_exists($algo, $this->supportedAlgos)) {
322            throw new Exception\InvalidArgumentException(sprintf(
323                'The algorithm %s is not supported by %s',
324                $algo,
325                __CLASS__
326            ));
327        }
328        $this->algo = $algo;
329
330        return $this;
331    }
332
333    /**
334     * Get the encryption algorithm
335     *
336     * @return string
337     */
338    public function getAlgorithm()
339    {
340        return $this->algo;
341    }
342
343    /**
344     * Set the padding object
345     *
346     * @param  Padding\PaddingInterface $padding
347     * @return Mcrypt Provides a fluent interface
348     */
349    public function setPadding(Padding\PaddingInterface $padding)
350    {
351        $this->padding = $padding;
352
353        return $this;
354    }
355
356    /**
357     * Get the padding object
358     *
359     * @return Padding\PaddingInterface
360     */
361    public function getPadding()
362    {
363        return $this->padding;
364    }
365
366    /**
367     * Encrypt
368     *
369     * @param  string $data
370     * @throws Exception\InvalidArgumentException
371     * @return string
372     */
373    public function encrypt($data)
374    {
375        // Cannot encrypt empty string
376        if (! is_string($data) || $data === '') {
377            throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty');
378        }
379        if (null === $this->getKey()) {
380            throw new Exception\InvalidArgumentException('No key specified for the encryption');
381        }
382        if (null === $this->getSalt()) {
383            throw new Exception\InvalidArgumentException('The salt (IV) cannot be empty');
384        }
385        if (null === $this->getPadding()) {
386            throw new Exception\InvalidArgumentException('You have to specify a padding method');
387        }
388        // padding
389        $data = $this->padding->pad($data, $this->getBlockSize());
390        $iv   = $this->getSalt();
391        // encryption
392        $result = mcrypt_encrypt(
393            $this->supportedAlgos[$this->algo],
394            $this->getKey(),
395            $data,
396            $this->supportedModes[$this->mode],
397            $iv
398        );
399
400        return $iv . $result;
401    }
402
403    /**
404     * Decrypt
405     *
406     * @param  string                             $data
407     * @throws Exception\InvalidArgumentException
408     * @return string
409     */
410    public function decrypt($data)
411    {
412        if (empty($data)) {
413            throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty');
414        }
415        if (null === $this->getKey()) {
416            throw new Exception\InvalidArgumentException('No key specified for the decryption');
417        }
418        if (null === $this->getPadding()) {
419            throw new Exception\InvalidArgumentException('You have to specify a padding method');
420        }
421        $iv         = mb_substr($data, 0, $this->getSaltSize(), '8bit');
422        $ciphertext = mb_substr($data, $this->getSaltSize(), null, '8bit');
423        $result     = mcrypt_decrypt(
424            $this->supportedAlgos[$this->algo],
425            $this->getKey(),
426            $ciphertext,
427            $this->supportedModes[$this->mode],
428            $iv
429        );
430        // unpadding
431        return $this->padding->strip($result);
432    }
433
434    /**
435     * Get the salt (IV) size
436     *
437     * @return int
438     */
439    public function getSaltSize()
440    {
441        return mcrypt_get_iv_size($this->supportedAlgos[$this->algo], $this->supportedModes[$this->mode]);
442    }
443
444    /**
445     * Get the supported algorithms
446     *
447     * @return array
448     */
449    public function getSupportedAlgorithms()
450    {
451        return array_keys($this->supportedAlgos);
452    }
453
454    /**
455     * Set the salt (IV)
456     *
457     * @param  string $salt
458     * @return Mcrypt Provides a fluent interface
459     * @throws Exception\InvalidArgumentException
460     */
461    public function setSalt($salt)
462    {
463        if (empty($salt)) {
464            throw new Exception\InvalidArgumentException('The salt (IV) cannot be empty');
465        }
466        if (mb_strlen($salt, '8bit') < $this->getSaltSize()) {
467            throw new Exception\InvalidArgumentException(sprintf(
468                'The size of the salt (IV) must be at least %d bytes',
469                $this->getSaltSize()
470            ));
471        }
472        $this->iv = $salt;
473
474        return $this;
475    }
476
477    /**
478     * Get the salt (IV) according to the size requested by the algorithm
479     *
480     * @return string
481     */
482    public function getSalt()
483    {
484        if (empty($this->iv)) {
485            return;
486        }
487        if (mb_strlen($this->iv, '8bit') < $this->getSaltSize()) {
488            throw new Exception\RuntimeException(sprintf(
489                'The size of the salt (IV) must be at least %d bytes',
490                $this->getSaltSize()
491            ));
492        }
493
494        return mb_substr($this->iv, 0, $this->getSaltSize(), '8bit');
495    }
496
497    /**
498     * Get the original salt value
499     *
500     * @return string
501     */
502    public function getOriginalSalt()
503    {
504        return $this->iv;
505    }
506
507    /**
508     * Set the cipher mode
509     *
510     * @param  string $mode
511     * @return Mcrypt Provides a fluent interface
512     * @throws Exception\InvalidArgumentException
513     */
514    public function setMode($mode)
515    {
516        if (! empty($mode)) {
517            $mode = strtolower($mode);
518            if (! array_key_exists($mode, $this->supportedModes)) {
519                throw new Exception\InvalidArgumentException(sprintf(
520                    'The mode %s is not supported by %s',
521                    $mode,
522                    $this->algo
523                ));
524            }
525            $this->mode = $mode;
526        }
527
528        return $this;
529    }
530
531    /**
532     * Get the cipher mode
533     *
534     * @return string
535     */
536    public function getMode()
537    {
538        return $this->mode;
539    }
540
541    /**
542     * Get all supported encryption modes
543     *
544     * @return array
545     */
546    public function getSupportedModes()
547    {
548        return array_keys($this->supportedModes);
549    }
550
551    /**
552     * Get the block size
553     *
554     * @return int
555     */
556    public function getBlockSize()
557    {
558        return mcrypt_get_block_size($this->supportedAlgos[$this->algo], $this->supportedModes[$this->mode]);
559    }
560}
561