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