Subversion-Projekte lars-tiefland.ci

Revision

Revision 1257 | Revision 2242 | Zur aktuellen Revision | Blame | Vergleich mit vorheriger | Letzte Änderung | Log anzeigen | RSS feed

<?php
/**
 * CodeIgniter
 *
 * An open source application development framework for PHP
 *
 * This content is released under the MIT License (MIT)
 *
 * Copyright (c) 2014 - 2017, British Columbia Institute of Technology
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 *
 * @package     CodeIgniter
 * @author      EllisLab Dev Team
 * @copyright   Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/)
 * @copyright   Copyright (c) 2014 - 2017, British Columbia Institute of Technology (http://bcit.ca/)
 * @license     http://opensource.org/licenses/MIT      MIT License
 * @link        https://codeigniter.com
 * @since       Version 3.0.0
 * @filesource
 */
defined('BASEPATH') OR exit('No direct script access allowed');

/**
 * CodeIgniter Encryption Class
 *
 * Provides two-way keyed encryption via PHP's MCrypt and/or OpenSSL extensions.
 *
 * @package             CodeIgniter
 * @subpackage  Libraries
 * @category    Libraries
 * @author              Andrey Andreev
 * @link                https://codeigniter.com/user_guide/libraries/encryption.html
 */
class CI_Encryption {

        /**
         * Encryption cipher
         *
         * @var string
         */
        protected $_cipher = 'aes-128';

        /**
         * Cipher mode
         *
         * @var string
         */
        protected $_mode = 'cbc';

        /**
         * Cipher handle
         *
         * @var mixed
         */
        protected $_handle;

        /**
         * Encryption key
         *
         * @var string
         */
        protected $_key;

        /**
         * PHP extension to be used
         *
         * @var string
         */
        protected $_driver;

        /**
         * List of usable drivers (PHP extensions)
         *
         * @var array
         */
        protected $_drivers = array();

        /**
         * List of available modes
         *
         * @var array
         */
        protected $_modes = array(
                'mcrypt' => array(
                        'cbc' => 'cbc',
                        'ecb' => 'ecb',
                        'ofb' => 'nofb',
                        'ofb8' => 'ofb',
                        'cfb' => 'ncfb',
                        'cfb8' => 'cfb',
                        'ctr' => 'ctr',
                        'stream' => 'stream'
                ),
                'openssl' => array(
                        'cbc' => 'cbc',
                        'ecb' => 'ecb',
                        'ofb' => 'ofb',
                        'cfb' => 'cfb',
                        'cfb8' => 'cfb8',
                        'ctr' => 'ctr',
                        'stream' => '',
                        'xts' => 'xts'
                )
        );

        /**
         * List of supported HMAC algorithms
         *
         * name => digest size pairs
         *
         * @var array
         */
        protected $_digests = array(
                'sha224' => 28,
                'sha256' => 32,
                'sha384' => 48,
                'sha512' => 64
        );

        /**
         * mbstring.func_override flag
         *
         * @var bool
         */
        protected static $func_override;

        // --------------------------------------------------------------------

        /**
         * Class constructor
         *
         * @param       array   $params Configuration parameters
         * @return      void
         */
        public function __construct(array $params = array())
        {
                $this->_drivers = array(
                        'mcrypt'  => defined('MCRYPT_DEV_URANDOM'),
                        'openssl' => extension_loaded('openssl')
                );

                if ( ! $this->_drivers['mcrypt'] && ! $this->_drivers['openssl'])
                {
                        show_error('Encryption: Unable to find an available encryption driver.');
                }

                isset(self::$func_override) OR self::$func_override = (extension_loaded('mbstring') && ini_get('mbstring.func_override'));
                $this->initialize($params);

                if ( ! isset($this->_key) && self::strlen($key = config_item('encryption_key')) > 0)
                {
                        $this->_key = $key;
                }

                log_message('info', 'Encryption Class Initialized');
        }

        // --------------------------------------------------------------------

        /**
         * Initialize
         *
         * @param       array   $params Configuration parameters
         * @return      CI_Encryption
         */
        public function initialize(array $params)
        {
                if ( ! empty($params['driver']))
                {
                        if (isset($this->_drivers[$params['driver']]))
                        {
                                if ($this->_drivers[$params['driver']])
                                {
                                        $this->_driver = $params['driver'];
                                }
                                else
                                {
                                        log_message('error', "Encryption: Driver '".$params['driver']."' is not available.");
                                }
                        }
                        else
                        {
                                log_message('error', "Encryption: Unknown driver '".$params['driver']."' cannot be configured.");
                        }
                }

                if (empty($this->_driver))
                {
                        $this->_driver = ($this->_drivers['openssl'] === TRUE)
                                ? 'openssl'
                                : 'mcrypt';

                        log_message('debug', "Encryption: Auto-configured driver '".$this->_driver."'.");
                }

                empty($params['cipher']) && $params['cipher'] = $this->_cipher;
                empty($params['key']) OR $this->_key = $params['key'];
                $this->{'_'.$this->_driver.'_initialize'}($params);
                return $this;
        }

        // --------------------------------------------------------------------

        /**
         * Initialize MCrypt
         *
         * @param       array   $params Configuration parameters
         * @return      void
         */
        protected function _mcrypt_initialize($params)
        {
                if ( ! empty($params['cipher']))
                {
                        $params['cipher'] = strtolower($params['cipher']);
                        $this->_cipher_alias($params['cipher']);

                        if ( ! in_array($params['cipher'], mcrypt_list_algorithms(), TRUE))
                        {
                                log_message('error', 'Encryption: MCrypt cipher '.strtoupper($params['cipher']).' is not available.');
                        }
                        else
                        {
                                $this->_cipher = $params['cipher'];
                        }
                }

                if ( ! empty($params['mode']))
                {
                        $params['mode'] = strtolower($params['mode']);
                        if ( ! isset($this->_modes['mcrypt'][$params['mode']]))
                        {
                                log_message('error', 'Encryption: MCrypt mode '.strtoupper($params['mode']).' is not available.');
                        }
                        else
                        {
                                $this->_mode = $this->_modes['mcrypt'][$params['mode']];
                        }
                }

                if (isset($this->_cipher, $this->_mode))
                {
                        if (is_resource($this->_handle)
                                && (strtolower(mcrypt_enc_get_algorithms_name($this->_handle)) !== $this->_cipher
                                        OR strtolower(mcrypt_enc_get_modes_name($this->_handle)) !== $this->_mode)
                        )
                        {
                                mcrypt_module_close($this->_handle);
                        }

                        if ($this->_handle = mcrypt_module_open($this->_cipher, '', $this->_mode, ''))
                        {
                                log_message('info', 'Encryption: MCrypt cipher '.strtoupper($this->_cipher).' initialized in '.strtoupper($this->_mode).' mode.');
                        }
                        else
                        {
                                log_message('error', 'Encryption: Unable to initialize MCrypt with cipher '.strtoupper($this->_cipher).' in '.strtoupper($this->_mode).' mode.');
                        }
                }
        }

        // --------------------------------------------------------------------

        /**
         * Initialize OpenSSL
         *
         * @param       array   $params Configuration parameters
         * @return      void
         */
        protected function _openssl_initialize($params)
        {
                if ( ! empty($params['cipher']))
                {
                        $params['cipher'] = strtolower($params['cipher']);
                        $this->_cipher_alias($params['cipher']);
                        $this->_cipher = $params['cipher'];
                }

                if ( ! empty($params['mode']))
                {
                        $params['mode'] = strtolower($params['mode']);
                        if ( ! isset($this->_modes['openssl'][$params['mode']]))
                        {
                                log_message('error', 'Encryption: OpenSSL mode '.strtoupper($params['mode']).' is not available.');
                        }
                        else
                        {
                                $this->_mode = $this->_modes['openssl'][$params['mode']];
                        }
                }

                if (isset($this->_cipher, $this->_mode))
                {
                        // This is mostly for the stream mode, which doesn't get suffixed in OpenSSL
                        $handle = empty($this->_mode)
                                ? $this->_cipher
                                : $this->_cipher.'-'.$this->_mode;

                        if ( ! in_array($handle, openssl_get_cipher_methods(), TRUE))
                        {
                                $this->_handle = NULL;
                                log_message('error', 'Encryption: Unable to initialize OpenSSL with method '.strtoupper($handle).'.');
                        }
                        else
                        {
                                $this->_handle = $handle;
                                log_message('info', 'Encryption: OpenSSL initialized with method '.strtoupper($handle).'.');
                        }
                }
        }

        // --------------------------------------------------------------------

        /**
         * Create a random key
         *
         * @param       int     $length Output length
         * @return      string
         */
        public function create_key($length)
        {
                if (function_exists('random_bytes'))
                {
                        try
                        {
                                return random_bytes((int) $length);
                        }
                        catch (Exception $e)
                        {
                                log_message('error', $e->getMessage());
                                return FALSE;
                        }
                }
                elseif (defined('MCRYPT_DEV_URANDOM'))
                {
                        return mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
                }

                $is_secure = NULL;
                $key = openssl_random_pseudo_bytes($length, $is_secure);
                return ($is_secure === TRUE)
                        ? $key
                        : FALSE;
        }

        // --------------------------------------------------------------------

        /**
         * Encrypt
         *
         * @param       string  $data   Input data
         * @param       array   $params Input parameters
         * @return      string
         */
        public function encrypt($data, array $params = NULL)
        {
                if (($params = $this->_get_params($params)) === FALSE)
                {
                        return FALSE;
                }

                isset($params['key']) OR $params['key'] = $this->hkdf($this->_key, 'sha512', NULL, self::strlen($this->_key), 'encryption');

                if (($data = $this->{'_'.$this->_driver.'_encrypt'}($data, $params)) === FALSE)
                {
                        return FALSE;
                }

                $params['base64'] && $data = base64_encode($data);

                if (isset($params['hmac_digest']))
                {
                        isset($params['hmac_key']) OR $params['hmac_key'] = $this->hkdf($this->_key, 'sha512', NULL, NULL, 'authentication');
                        return hash_hmac($params['hmac_digest'], $data, $params['hmac_key'], ! $params['base64']).$data;
                }

                return $data;
        }

        // --------------------------------------------------------------------

        /**
         * Encrypt via MCrypt
         *
         * @param       string  $data   Input data
         * @param       array   $params Input parameters
         * @return      string
         */
        protected function _mcrypt_encrypt($data, $params)
        {
                if ( ! is_resource($params['handle']))
                {
                        return FALSE;
                }

                // The greater-than-1 comparison is mostly a work-around for a bug,
                // where 1 is returned for ARCFour instead of 0.
                $iv = (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1)
                        ? $this->create_key($iv_size)
                        : NULL;

                if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0)
                {
                        if ($params['handle'] !== $this->_handle)
                        {
                                mcrypt_module_close($params['handle']);
                        }

                        return FALSE;
                }

                // Use PKCS#7 padding in order to ensure compatibility with OpenSSL
                // and other implementations outside of PHP.
                if (in_array(strtolower(mcrypt_enc_get_modes_name($params['handle'])), array('cbc', 'ecb'), TRUE))
                {
                        $block_size = mcrypt_enc_get_block_size($params['handle']);
                        $pad = $block_size - (self::strlen($data) % $block_size);
                        $data .= str_repeat(chr($pad), $pad);
                }

                // Work-around for yet another strange behavior in MCrypt.
                //
                // When encrypting in ECB mode, the IV is ignored. Yet
                // mcrypt_enc_get_iv_size() returns a value larger than 0
                // even if ECB is used AND mcrypt_generic_init() complains
                // if you don't pass an IV with length equal to the said
                // return value.
                //
                // This probably would've been fine (even though still wasteful),
                // but OpenSSL isn't that dumb and we need to make the process
                // portable, so ...
                $data = (mcrypt_enc_get_modes_name($params['handle']) !== 'ECB')
                        ? $iv.mcrypt_generic($params['handle'], $data)
                        : mcrypt_generic($params['handle'], $data);

                mcrypt_generic_deinit($params['handle']);
                if ($params['handle'] !== $this->_handle)
                {
                        mcrypt_module_close($params['handle']);
                }

                return $data;
        }

        // --------------------------------------------------------------------

        /**
         * Encrypt via OpenSSL
         *
         * @param       string  $data   Input data
         * @param       array   $params Input parameters
         * @return      string
         */
        protected function _openssl_encrypt($data, $params)
        {
                if (empty($params['handle']))
                {
                        return FALSE;
                }

                $iv = ($iv_size = openssl_cipher_iv_length($params['handle']))
                        ? $this->create_key($iv_size)
                        : NULL;

                $data = openssl_encrypt(
                        $data,
                        $params['handle'],
                        $params['key'],
                        1, // DO NOT TOUCH!
                        $iv
                );

                if ($data === FALSE)
                {
                        return FALSE;
                }

                return $iv.$data;
        }

        // --------------------------------------------------------------------

        /**
         * Decrypt
         *
         * @param       string  $data   Encrypted data
         * @param       array   $params Input parameters
         * @return      string
         */
        public function decrypt($data, array $params = NULL)
        {
                if (($params = $this->_get_params($params)) === FALSE)
                {
                        return FALSE;
                }

                if (isset($params['hmac_digest']))
                {
                        // This might look illogical, but it is done during encryption as well ...
                        // The 'base64' value is effectively an inverted "raw data" parameter
                        $digest_size = ($params['base64'])
                                ? $this->_digests[$params['hmac_digest']] * 2
                                : $this->_digests[$params['hmac_digest']];

                        if (self::strlen($data) <= $digest_size)
                        {
                                return FALSE;
                        }

                        $hmac_input = self::substr($data, 0, $digest_size);
                        $data = self::substr($data, $digest_size);

                        isset($params['hmac_key']) OR $params['hmac_key'] = $this->hkdf($this->_key, 'sha512', NULL, NULL, 'authentication');
                        $hmac_check = hash_hmac($params['hmac_digest'], $data, $params['hmac_key'], ! $params['base64']);

                        // Time-attack-safe comparison
                        $diff = 0;
                        for ($i = 0; $i < $digest_size; $i++)
                        {
                                $diff |= ord($hmac_input[$i]) ^ ord($hmac_check[$i]);
                        }

                        if ($diff !== 0)
                        {
                                return FALSE;
                        }
                }

                if ($params['base64'])
                {
                        $data = base64_decode($data);
                }

                isset($params['key']) OR $params['key'] = $this->hkdf($this->_key, 'sha512', NULL, self::strlen($this->_key), 'encryption');

                return $this->{'_'.$this->_driver.'_decrypt'}($data, $params);
        }

        // --------------------------------------------------------------------

        /**
         * Decrypt via MCrypt
         *
         * @param       string  $data   Encrypted data
         * @param       array   $params Input parameters
         * @return      string
         */
        protected function _mcrypt_decrypt($data, $params)
        {
                if ( ! is_resource($params['handle']))
                {
                        return FALSE;
                }

                // The greater-than-1 comparison is mostly a work-around for a bug,
                // where 1 is returned for ARCFour instead of 0.
                if (($iv_size = mcrypt_enc_get_iv_size($params['handle'])) > 1)
                {
                        if (mcrypt_enc_get_modes_name($params['handle']) !== 'ECB')
                        {
                                $iv = self::substr($data, 0, $iv_size);
                                $data = self::substr($data, $iv_size);
                        }
                        else
                        {
                                // MCrypt is dumb and this is ignored, only size matters
                                $iv = str_repeat("\x0", $iv_size);
                        }
                }
                else
                {
                        $iv = NULL;
                }

                if (mcrypt_generic_init($params['handle'], $params['key'], $iv) < 0)
                {
                        if ($params['handle'] !== $this->_handle)
                        {
                                mcrypt_module_close($params['handle']);
                        }

                        return FALSE;
                }

                $data = mdecrypt_generic($params['handle'], $data);
                // Remove PKCS#7 padding, if necessary
                if (in_array(strtolower(mcrypt_enc_get_modes_name($params['handle'])), array('cbc', 'ecb'), TRUE))
                {
                        $data = self::substr($data, 0, -ord($data[self::strlen($data)-1]));
                }

                mcrypt_generic_deinit($params['handle']);
                if ($params['handle'] !== $this->_handle)
                {
                        mcrypt_module_close($params['handle']);
                }

                return $data;
        }

        // --------------------------------------------------------------------

        /**
         * Decrypt via OpenSSL
         *
         * @param       string  $data   Encrypted data
         * @param       array   $params Input parameters
         * @return      string
         */
        protected function _openssl_decrypt($data, $params)
        {
                if ($iv_size = openssl_cipher_iv_length($params['handle']))
                {
                        $iv = self::substr($data, 0, $iv_size);
                        $data = self::substr($data, $iv_size);
                }
                else
                {
                        $iv = NULL;
                }

                return empty($params['handle'])
                        ? FALSE
                        : openssl_decrypt(
                                $data,
                                $params['handle'],
                                $params['key'],
                                1, // DO NOT TOUCH!
                                $iv
                        );
        }

        // --------------------------------------------------------------------

        /**
         * Get params
         *
         * @param       array   $params Input parameters
         * @return      array
         */
        protected function _get_params($params)
        {
                if (empty($params))
                {
                        return isset($this->_cipher, $this->_mode, $this->_key, $this->_handle)
                                ? array(
                                        'handle' => $this->_handle,
                                        'cipher' => $this->_cipher,
                                        'mode' => $this->_mode,
                                        'key' => NULL,
                                        'base64' => TRUE,
                                        'hmac_digest' => 'sha512',
                                        'hmac_key' => NULL
                                )
                                : FALSE;
                }
                elseif ( ! isset($params['cipher'], $params['mode'], $params['key']))
                {
                        return FALSE;
                }

                if (isset($params['mode']))
                {
                        $params['mode'] = strtolower($params['mode']);
                        if ( ! isset($this->_modes[$this->_driver][$params['mode']]))
                        {
                                return FALSE;
                        }
                        else
                        {
                                $params['mode'] = $this->_modes[$this->_driver][$params['mode']];
                        }
                }

                if (isset($params['hmac']) && $params['hmac'] === FALSE)
                {
                        $params['hmac_digest'] = $params['hmac_key'] = NULL;
                }
                else
                {
                        if ( ! isset($params['hmac_key']))
                        {
                                return FALSE;
                        }
                        elseif (isset($params['hmac_digest']))
                        {
                                $params['hmac_digest'] = strtolower($params['hmac_digest']);
                                if ( ! isset($this->_digests[$params['hmac_digest']]))
                                {
                                        return FALSE;
                                }
                        }
                        else
                        {
                                $params['hmac_digest'] = 'sha512';
                        }
                }

                $params = array(
                        'handle' => NULL,
                        'cipher' => $params['cipher'],
                        'mode' => $params['mode'],
                        'key' => $params['key'],
                        'base64' => isset($params['raw_data']) ? ! $params['raw_data'] : FALSE,
                        'hmac_digest' => $params['hmac_digest'],
                        'hmac_key' => $params['hmac_key']
                );

                $this->_cipher_alias($params['cipher']);
                $params['handle'] = ($params['cipher'] !== $this->_cipher OR $params['mode'] !== $this->_mode)
                        ? $this->{'_'.$this->_driver.'_get_handle'}($params['cipher'], $params['mode'])
                        : $this->_handle;

                return $params;
        }

        // --------------------------------------------------------------------

        /**
         * Get MCrypt handle
         *
         * @param       string  $cipher Cipher name
         * @param       string  $mode   Encryption mode
         * @return      resource
         */
        protected function _mcrypt_get_handle($cipher, $mode)
        {
                return mcrypt_module_open($cipher, '', $mode, '');
        }

        // --------------------------------------------------------------------

        /**
         * Get OpenSSL handle
         *
         * @param       string  $cipher Cipher name
         * @param       string  $mode   Encryption mode
         * @return      string
         */
        protected function _openssl_get_handle($cipher, $mode)
        {
                // OpenSSL methods aren't suffixed with '-stream' for this mode
                return ($mode === 'stream')
                        ? $cipher
                        : $cipher.'-'.$mode;
        }

        // --------------------------------------------------------------------

        /**
         * Cipher alias
         *
         * Tries to translate cipher names between MCrypt and OpenSSL's "dialects".
         *
         * @param       string  $cipher Cipher name
         * @return      void
         */
        protected function _cipher_alias(&$cipher)
        {
                static $dictionary;

                if (empty($dictionary))
                {
                        $dictionary = array(
                                'mcrypt' => array(
                                        'aes-128' => 'rijndael-128',
                                        'aes-192' => 'rijndael-128',
                                        'aes-256' => 'rijndael-128',
                                        'des3-ede3' => 'tripledes',
                                        'bf' => 'blowfish',
                                        'cast5' => 'cast-128',
                                        'rc4' => 'arcfour',
                                        'rc4-40' => 'arcfour'
                                ),
                                'openssl' => array(
                                        'rijndael-128' => 'aes-128',
                                        'tripledes' => 'des-ede3',
                                        'blowfish' => 'bf',
                                        'cast-128' => 'cast5',
                                        'arcfour' => 'rc4-40',
                                        'rc4' => 'rc4-40'
                                )
                        );

                        // Notes:
                        //
                        // - Rijndael-128 is, at the same time all three of AES-128,
                        //   AES-192 and AES-256. The only difference between them is
                        //   the key size. Rijndael-192, Rijndael-256 on the other hand
                        //   also have different block sizes and are NOT AES-compatible.
                        //
                        // - Blowfish is said to be supporting key sizes between
                        //   4 and 56 bytes, but it appears that between MCrypt and
                        //   OpenSSL, only those of 16 and more bytes are compatible.
                        //   Also, don't know what MCrypt's 'blowfish-compat' is.
                        //
                        // - CAST-128/CAST5 produces a longer cipher when encrypted via
                        //   OpenSSL, but (strangely enough) can be decrypted by either
                        //   extension anyway.
                        //   Also, it appears that OpenSSL uses 16 rounds regardless of
                        //   the key size, while RFC2144 says that for key sizes lower
                        //   than 11 bytes, only 12 rounds should be used. This makes
                        //   it portable only with keys of between 11 and 16 bytes.
                        //
                        // - RC4 (ARCFour) has a strange implementation under OpenSSL.
                        //   Its 'rc4-40' cipher method seems to work flawlessly, yet
                        //   there's another one, 'rc4' that only works with a 16-byte key.
                        //
                        // - DES is compatible, but doesn't need an alias.
                        //
                        // Other seemingly matching ciphers between MCrypt, OpenSSL:
                        //
                        // - RC2 is NOT compatible and only an obscure forum post
                        //   confirms that it is MCrypt's fault.
                }

                if (isset($dictionary[$this->_driver][$cipher]))
                {
                        $cipher = $dictionary[$this->_driver][$cipher];
                }
        }

        // --------------------------------------------------------------------

        /**
         * HKDF
         *
         * @link        https://tools.ietf.org/rfc/rfc5869.txt
         * @param       $key    Input key
         * @param       $digest A SHA-2 hashing algorithm
         * @param       $salt   Optional salt
         * @param       $length Output length (defaults to the selected digest size)
         * @param       $info   Optional context/application-specific info
         * @return      string  A pseudo-random key
         */
        public function hkdf($key, $digest = 'sha512', $salt = NULL, $length = NULL, $info = '')
        {
                if ( ! isset($this->_digests[$digest]))
                {
                        return FALSE;
                }

                if (empty($length) OR ! is_int($length))
                {
                        $length = $this->_digests[$digest];
                }
                elseif ($length > (255 * $this->_digests[$digest]))
                {
                        return FALSE;
                }

                self::strlen($salt) OR $salt = str_repeat("\0", $this->_digests[$digest]);

                $prk = hash_hmac($digest, $key, $salt, TRUE);
                $key = '';
                for ($key_block = '', $block_index = 1; self::strlen($key) < $length; $block_index++)
                {
                        $key_block = hash_hmac($digest, $key_block.$info.chr($block_index), $prk, TRUE);
                        $key .= $key_block;
                }

                return self::substr($key, 0, $length);
        }

        // --------------------------------------------------------------------

        /**
         * __get() magic
         *
         * @param       string  $key    Property name
         * @return      mixed
         */
        public function __get($key)
        {
                // Because aliases
                if ($key === 'mode')
                {
                        return array_search($this->_mode, $this->_modes[$this->_driver], TRUE);
                }
                elseif (in_array($key, array('cipher', 'driver', 'drivers', 'digests'), TRUE))
                {
                        return $this->{'_'.$key};
                }

                return NULL;
        }

        // --------------------------------------------------------------------

        /**
         * Byte-safe strlen()
         *
         * @param       string  $str
         * @return      int
         */
        protected static function strlen($str)
        {
                return (self::$func_override)
                        ? mb_strlen($str, '8bit')
                        : strlen($str);
        }

        // --------------------------------------------------------------------

        /**
         * Byte-safe substr()
         *
         * @param       string  $str
         * @param       int     $start
         * @param       int     $length
         * @return      string
         */
        protected static function substr($str, $start, $length = NULL)
        {
                if (self::$func_override)
                {
                        // mb_substr($str, $start, null, '8bit') returns an empty
                        // string on PHP 5.3
                        isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start);
                        return mb_substr($str, $start, $length, '8bit');
                }

                return isset($length)
                        ? substr($str, $start, $length)
                        : substr($str, $start);
        }
}