<?php /** * Zend Framework (http://framework.zend.com/) * * @link http://github.com/zendframework/zf2 for the canonical source repository * @copyright Copyright (c) 2005-2014 Zend Technologies USA Inc. (http://www.zend.com) * @license http://framework.zend.com/license/new-bsd New BSD License */ namespace Zend\Crypt\Symmetric; use Traversable; use Zend\Stdlib\ArrayUtils; /** * Symmetric encryption using the Mcrypt extension * * NOTE: DO NOT USE only this class to encrypt data. * This class doesn't provide authentication and integrity check over the data. * PLEASE USE Zend\Crypt\BlockCipher instead! */ class Mcrypt implements SymmetricInterface { const DEFAULT_PADDING = 'pkcs7'; /** * Key * * @var string */ protected $key; /** * IV * * @var string */ protected $iv; /** * Encryption algorithm * * @var string */ protected $algo = 'aes'; /** * Encryption mode * * @var string */ protected $mode = 'cbc'; /** * Padding * * @var Padding\PaddingInterface */ protected $padding; /** * Padding plugins * * @var PaddingPluginManager */ protected static $paddingPlugins = null; /** * Supported cipher algorithms * * @var array */ protected $supportedAlgos = array( 'aes' => 'rijndael-128', 'blowfish' => 'blowfish', 'des' => 'des', '3des' => 'tripledes', 'tripledes' => 'tripledes', 'cast-128' => 'cast-128', 'cast-256' => 'cast-256', 'rijndael-128' => 'rijndael-128', 'rijndael-192' => 'rijndael-192', 'rijndael-256' => 'rijndael-256', 'saferplus' => 'saferplus', 'serpent' => 'serpent', 'twofish' => 'twofish' ); /** * Supported encryption modes * * @var array */ protected $supportedModes = array( 'cbc' => 'cbc', 'cfb' => 'cfb', 'ctr' => 'ctr', 'ofb' => 'ofb', 'nofb' => 'nofb', 'ncfb' => 'ncfb' ); /** * Constructor * * @param array|Traversable $options * @throws Exception\RuntimeException * @throws Exception\InvalidArgumentException */ public function __construct($options = array()) { if (!extension_loaded('mcrypt')) { throw new Exception\RuntimeException( 'You cannot use ' . __CLASS__ . ' without the Mcrypt extension' ); } if (!empty($options)) { if ($options instanceof Traversable) { $options = ArrayUtils::iteratorToArray($options); } elseif (!is_array($options)) { throw new Exception\InvalidArgumentException( 'The options parameter must be an array, a Zend\Config\Config object or a Traversable' ); } foreach ($options as $key => $value) { switch (strtolower($key)) { case 'algo': case 'algorithm': $this->setAlgorithm($value); break; case 'mode': $this->setMode($value); break; case 'key': $this->setKey($value); break; case 'iv': case 'salt': $this->setSalt($value); break; case 'padding': $plugins = static::getPaddingPluginManager(); $padding = $plugins->get($value); $this->padding = $padding; break; } } } $this->setDefaultOptions($options); } /** * Set default options * * @param array $options * @return void */ protected function setDefaultOptions($options = array()) { if (!isset($options['padding'])) { $plugins = static::getPaddingPluginManager(); $padding = $plugins->get(self::DEFAULT_PADDING); $this->padding = $padding; } } /** * Returns the padding plugin manager. If it doesn't exist it's created. * * @return PaddingPluginManager */ public static function getPaddingPluginManager() { if (static::$paddingPlugins === null) { self::setPaddingPluginManager(new PaddingPluginManager()); } return static::$paddingPlugins; } /** * Set the padding plugin manager * * @param string|PaddingPluginManager $plugins * @throws Exception\InvalidArgumentException * @return void */ public static function setPaddingPluginManager($plugins) { if (is_string($plugins)) { if (!class_exists($plugins)) { throw new Exception\InvalidArgumentException(sprintf( 'Unable to locate padding plugin manager via class "%s"; class does not exist', $plugins )); } $plugins = new $plugins(); } if (!$plugins instanceof PaddingPluginManager) { throw new Exception\InvalidArgumentException(sprintf( 'Padding plugins must extend %s\PaddingPluginManager; received "%s"', __NAMESPACE__, (is_object($plugins) ? get_class($plugins) : gettype($plugins)) )); } static::$paddingPlugins = $plugins; } /** * Get the maximum key size for the selected cipher and mode of operation * * @return int */ public function getKeySize() { return mcrypt_get_key_size($this->supportedAlgos[$this->algo], $this->supportedModes[$this->mode]); } /** * Set the encryption key * If the key is longer than maximum supported, it will be truncated by getKey(). * * @param string $key * @throws Exception\InvalidArgumentException * @return Mcrypt */ public function setKey($key) { $keyLen = strlen($key); if (!$keyLen) { throw new Exception\InvalidArgumentException('The key cannot be empty'); } $keySizes = mcrypt_module_get_supported_key_sizes($this->supportedAlgos[$this->algo]); $maxKey = $this->getKeySize(); /* * blowfish has $keySizes empty, meaning it can have arbitrary key length. * the others are more picky. */ if (!empty($keySizes) && $keyLen < $maxKey) { if (!in_array($keyLen, $keySizes)) { throw new Exception\InvalidArgumentException( "The size of the key must be one of " . implode(", ", $keySizes) . " bytes or longer"); } } $this->key = $key; return $this; } /** * Get the encryption key * * @return string */ public function getKey() { if (empty($this->key)) { return null; } return substr($this->key, 0, $this->getKeySize()); } /** * Set the encryption algorithm (cipher) * * @param string $algo * @throws Exception\InvalidArgumentException * @return Mcrypt */ public function setAlgorithm($algo) { if (!array_key_exists($algo, $this->supportedAlgos)) { throw new Exception\InvalidArgumentException( "The algorithm $algo is not supported by " . __CLASS__ ); } $this->algo = $algo; return $this; } /** * Get the encryption algorithm * * @return string */ public function getAlgorithm() { return $this->algo; } /** * Set the padding object * * @param Padding\PaddingInterface $padding * @return Mcrypt */ public function setPadding(Padding\PaddingInterface $padding) { $this->padding = $padding; return $this; } /** * Get the padding object * * @return Padding\PaddingInterface */ public function getPadding() { return $this->padding; } /** * Encrypt * * @param string $data * @throws Exception\InvalidArgumentException * @return string */ public function encrypt($data) { if (empty($data)) { throw new Exception\InvalidArgumentException('The data to encrypt cannot be empty'); } if (null === $this->getKey()) { throw new Exception\InvalidArgumentException('No key specified for the encryption'); } if (null === $this->getSalt()) { throw new Exception\InvalidArgumentException('The salt (IV) cannot be empty'); } if (null === $this->getPadding()) { throw new Exception\InvalidArgumentException('You have to specify a padding method'); } // padding $data = $this->padding->pad($data, $this->getBlockSize()); $iv = $this->getSalt(); // encryption $result = mcrypt_encrypt( $this->supportedAlgos[$this->algo], $this->getKey(), $data, $this->supportedModes[$this->mode], $iv ); return $iv . $result; } /** * Decrypt * * @param string $data * @throws Exception\InvalidArgumentException * @return string */ public function decrypt($data) { if (empty($data)) { throw new Exception\InvalidArgumentException('The data to decrypt cannot be empty'); } if (null === $this->getKey()) { throw new Exception\InvalidArgumentException('No key specified for the decryption'); } if (null === $this->getPadding()) { throw new Exception\InvalidArgumentException('You have to specify a padding method'); } $iv = substr($data, 0, $this->getSaltSize()); $ciphertext = substr($data, $this->getSaltSize()); $result = mcrypt_decrypt( $this->supportedAlgos[$this->algo], $this->getKey(), $ciphertext, $this->supportedModes[$this->mode], $iv ); // unpadding return $this->padding->strip($result); } /** * Get the salt (IV) size * * @return int */ public function getSaltSize() { return mcrypt_get_iv_size($this->supportedAlgos[$this->algo], $this->supportedModes[$this->mode]); } /** * Get the supported algorithms * * @return array */ public function getSupportedAlgorithms() { return array_keys($this->supportedAlgos); } /** * Set the salt (IV) * * @param string $salt * @throws Exception\InvalidArgumentException * @return Mcrypt */ public function setSalt($salt) { if (empty($salt)) { throw new Exception\InvalidArgumentException('The salt (IV) cannot be empty'); } if (strlen($salt) < $this->getSaltSize()) { throw new Exception\InvalidArgumentException( 'The size of the salt (IV) must be at least ' . $this->getSaltSize() . ' bytes' ); } $this->iv = $salt; return $this; } /** * Get the salt (IV) according to the size requested by the algorithm * * @return string */ public function getSalt() { if (empty($this->iv)) { return null; } if (strlen($this->iv) < $this->getSaltSize()) { throw new Exception\RuntimeException( 'The size of the salt (IV) must be at least ' . $this->getSaltSize() . ' bytes' ); } return substr($this->iv, 0, $this->getSaltSize()); } /** * Get the original salt value * * @return string */ public function getOriginalSalt() { return $this->iv; } /** * Set the cipher mode * * @param string $mode * @throws Exception\InvalidArgumentException * @return Mcrypt */ public function setMode($mode) { if (!empty($mode)) { $mode = strtolower($mode); if (!array_key_exists($mode, $this->supportedModes)) { throw new Exception\InvalidArgumentException( "The mode $mode is not supported by " . __CLASS__ ); } $this->mode = $mode; } return $this; } /** * Get the cipher mode * * @return string */ public function getMode() { return $this->mode; } /** * Get all supported encryption modes * * @return array */ public function getSupportedModes() { return array_keys($this->supportedModes); } /** * Get the block size * * @return int */ public function getBlockSize() { return mcrypt_get_block_size($this->supportedAlgos[$this->algo], $this->supportedModes[$this->mode]); } }