<?php
/**
 * A class for encrypting and decrypting strings.
 *
 * This class provides a way to encrypt or decrypt a string provided the
 * encryption algorithm, the mode and the secret key.
 */
class crypto
{
    /**
     * @var integer The vector source options.
     */
    const DEV_RANDOM  = MCRYPT_DEV_RANDOM;
    const DEV_URANDOM = MCRYPT_DEV_URANDOM;
    const RANDOM      = MCRYPT_RAND;

    /**
     * @var resource The cipher resource.
     */
    protected $cipher;

    ////////////////////////////////////////////////////////////////////////////
    // Construct and destruct functions.

    /**
     * @param string (optional) $key The secret key.
     * @param string/array (optional) $algorithm The encryption algorithm.
     * @param string/array (optional) $mode The mode.
     * @param string (optional) $vector The initialization vector.
     * @param string (optional) $source The vector source.
     */
    public function __construct($key = null, $algorithm = 'tripledes', $mode = 'ecb', $vector = null, $source = self::RANDOM)
    {
        if ($key !== null)
        {
            $this->open($key, $algorithm, $mode, $vector, $source);
        }
    }

    public function __destruct()
    {
        $this->close();
    }

    ////////////////////////////////////////////////////////////////////////////
    // Interface functions.

    /**
     * Opens the cipher.
     *
     * @param string $key The secret key.
     * @param string/array $algorithm (optional) The encryption algorithm.
     * @param string/array (optional) $mode The mode.
     * @param string (optional) $vector The initialization vector.
     * @param integer (optional) $source The vector source.
     * @throws lengthexception If the secret key is not of valid length.
     * @throws unexpectedvalueexception If the cipher could not be initialized.
     * @return crypto A fluent interface.
     */
    public function open($key, $algorithm = 'tripledes', $mode = 'ecb', $vector = null, $source = self::RANDOM)
    {
        $this->close();
        $this->cipher = $this->create_cipher($algorithm, $mode);
        
        if (strlen($key) !== mcrypt_enc_get_key_size($this->cipher))
        {
            throw new lengthexception('Secret key given does not meet algorithm\'s expected key length.');
        }
        
        if ($vector === null)
        {
            $vector = $this->create_vector($source);
        }
        
        $initialized = mcrypt_generic_init($this->cipher, $key, $vector);
        
        if($initialized < 0 || $initialized === false)
        {
            throw new unexpectedvalueexception('Cipher could not be initialized.');
        }
        
        return $this;
    }

    /**
     * Closes the cipher.
     * 
     * @return crypto A fluent interface.
     */
    public function close()
    {
        if ($this->cipher)
        {
            mcrypt_generic_deinit($this->cipher);
            mcrypt_module_close($this->cipher);
        }
        
        $this->cipher = null;
        return $this;
    }

    /**
     * Encrypts a string.
     * 
     * @param string $value The string.
     * @throws logicexception If the crypto object is not opened.
     * @return string The encrypted string.
     */
    public function encrypt($value)
    {
        if (!$this->cipher)
        {
            throw new logicexception('Crypto is not opened.');
        }
        
        $value = (string) $value;
        
        if ($value === '')
        {
            return '';
        }
        
        return base64_encode(mcrypt_generic($this->cipher, $value));
    }

    /**
     * Decrypts an encrypted string.
     * 
     * @param string $value The encrypted string.
     * @throws logicexception If the crypto object is not opened.
     * @return string The decrypted string.
     */
    public function decrypt($value)
    {
        if (!$this->cipher)
        {
            throw new logicexception('Crypto is not opened.');
        }
        
        $value = base64_decode($value);
        
        if ($value === '')
        {
            return '';
        }
        
        return rtrim(mdecrypt_generic($this->cipher, $value));
    }

    ////////////////////////////////////////////////////////////////////////////
    // Internal functions.

    /**
     * Creates a cipher resource.
     * 
     * @param string/array $algorithm The encryption algorithm.
     * @param string/array $mode The mode.
     * @throws unexpectedvalueexception If the cipher resource couldnot be
     * created.
     * @return resource The cipher.
     */
    protected function create_cipher($algorithm, $mode)
    {
        list($an, $ap) = (is_array($algorithm)) ? $algorithm : array($algorithm, '');
        list($mn, $mp) = (is_array($mode)) ? $mode : array($mode, '');
        $cipher = mcrypt_module_open($an, $ap, $mn, $mp);
        
        if (!$cipher)
        {
            throw new unexpectedvalueexception('Cipher resource could not be created.');
        }
        
        return $cipher;
    }

    /**
     * Creates an initialization vector.
     * 
     * @param integer (optional) $source The vector source.
     * @throws logicexception If the crypto object is not opened.
     * @throws outofrangeexception If the vector source does not exist.
     * @return string The vector.
     */
    protected function create_vector($source = self::RANDOM)
    {
        if (!$this->cipher)
        {
            throw new logicexception('Crypto is not opened.');
        }
        
        $vector_size = mcrypt_enc_get_iv_size($this->cipher);
        
        if ($source !== self::DEV_RANDOM && $source !== self::DEV_URANDOM && $source !== self::RANDOM)
        {
            throw new outofrangeexception('Vector source ' . $source . ' does not exist.');
        }
        
        $vector = mcrypt_create_iv($vector_size, $source);
        return $vector;
    }
}
