Add an ext/hash compatibility layer (just hash_pbkdf2(), for now)
diff --git a/system/core/CodeIgniter.php b/system/core/CodeIgniter.php
index 1f10c45..2bdd764 100644
--- a/system/core/CodeIgniter.php
+++ b/system/core/CodeIgniter.php
@@ -189,6 +189,7 @@
  */
 
 	require_once(BASEPATH.'core/compat/mbstring.php');
+	require_once(BASEPATH.'core/compat/hash.php');
 	require_once(BASEPATH.'core/compat/password.php');
 
 /*
diff --git a/system/core/compat/hash.php b/system/core/compat/hash.php
new file mode 100644
index 0000000..a9f59f1
--- /dev/null
+++ b/system/core/compat/hash.php
@@ -0,0 +1,144 @@
+<?php
+/**
+ * CodeIgniter
+ *
+ * An open source application development framework for PHP 5.2.4 or newer
+ *
+ * NOTICE OF LICENSE
+ *
+ * Licensed under the Open Software License version 3.0
+ *
+ * This source file is subject to the Open Software License (OSL 3.0) that is
+ * bundled with this package in the files license.txt / license.rst.  It is
+ * also available through the world wide web at this URL:
+ * http://opensource.org/licenses/OSL-3.0
+ * If you did not receive a copy of the license and are unable to obtain it
+ * through the world wide web, please send an email to
+ * licensing@ellislab.com so we can send you a copy immediately.
+ *
+ * @package		CodeIgniter
+ * @author		EllisLab Dev Team
+ * @copyright	Copyright (c) 2008 - 2014, EllisLab, Inc. (http://ellislab.com/)
+ * @license		http://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
+ * @link		http://codeigniter.com
+ * @since		Version 3.0
+ * @filesource
+ */
+defined('BASEPATH') OR exit('No direct script access allowed');
+
+/**
+ * PHP ext/hash compatibility package
+ *
+ * @package		CodeIgniter
+ * @subpackage	CodeIgniter
+ * @category	Compatibility
+ * @author		Andrey Andreev
+ * @link		http://codeigniter.com/user_guide/
+ * @link		http://php.net/hash
+ */
+
+// ------------------------------------------------------------------------
+
+if (is_php('5.5'))
+{
+	return;
+}
+
+// ------------------------------------------------------------------------
+
+if ( ! function_exists('hash_pbkdf2'))
+{
+	/**
+	 * hash_pbkdf2()
+	 *
+	 * @link	http://php.net/hash_pbkdf2
+	 * @param	string	$algo
+	 * @param	string	$password
+	 * @param	string	$salt
+	 * @param	int	$iterations
+	 * @param	int	$length
+	 * @param	bool	$raw_output
+	 * @return	string
+	 */
+	function hash_pbkdf2($algo, $password, $salt, $iterations, $length = 0, $raw_output = FALSE)
+	{
+		if ( ! in_array($algo, hash_algos(), TRUE))
+		{
+			trigger_error('hash_pbkdf2(): Unknown hashing algorithm: '.$algo, E_USER_WARNING);
+			return FALSE;
+		}
+
+		if (($type = gettype($iterations)) !== 'integer')
+		{
+			if ($type === 'object' && method_exists($iterations, '__toString'))
+			{
+				$iterations = (string) $iterations;
+			}
+
+			if (is_string($iterations) && is_numeric($iterations))
+			{
+				$iterations = (int) $iterations;
+			}
+			else
+			{
+				trigger_error('hash_pbkdf2() expects parameter 4 to be long, '.$type.' given', E_USER_WARNING);
+				return NULL;
+			}
+		}
+
+		if ($iterations < 1)
+		{
+			trigger_error('hash_pbkdf2(): Iterations must be a positive integer: '.$iterations, E_USER_WARNING);
+			return FALSE;
+		}
+
+		if (($type = gettype($length)) !== 'integer')
+		{
+			if ($type === 'object' && method_exists($length, '__toString'))
+			{
+				$length = (string) $length;
+			}
+
+			if (is_string($length) && is_numeric($length))
+			{
+				$length = (int) $length;
+			}
+			else
+			{
+				trigger_error('hash_pbkdf2() expects parameter 5 to be long, '.$type.' given', E_USER_WARNING);
+				return NULL;
+			}
+		}
+
+		if ($length < 0)
+		{
+			trigger_error('hash_pbkdf2(): Length must be greater than or equal to 0: '.$length, E_USER_WARNING);
+			return FALSE;
+		}
+
+		$hash_length = strlen(hash($algo, NULL, TRUE));
+		if (empty($length))
+		{
+			$length = $hash_length;
+		}
+
+		$hash = '';
+		// Note: Blocks are NOT 0-indexed
+		for ($bc = ceil($length / $hash_length), $bi = 1; $bi <= $bc; $bi++)
+		{
+			$key = $derived_key = hash_hmac($algo, $salt.pack('N', $bi), $password, TRUE);
+			for ($i = 1; $i < $iterations; $i++)
+			{
+				$derived_key ^= $key = hash_hmac($algo, $key, $password, TRUE);
+			}
+
+			$hash .= $derived_key;
+		}
+
+		// This is not RFC-compatible, but we're aiming for natural PHP compatibility
+		return substr($raw_output ? $hash : bin2hex($hash), 0, $length);
+	}
+}
+
+/* End of file hash.php */
+/* Location: ./system/core/compat/hash.php */
\ No newline at end of file
diff --git a/tests/Bootstrap.php b/tests/Bootstrap.php
index 439c7fd..5441f71 100644
--- a/tests/Bootstrap.php
+++ b/tests/Bootstrap.php
@@ -64,6 +64,7 @@
 }
 
 include_once SYSTEM_PATH.'core/compat/mbstring.php';
+include_once SYSTEM_PATH.'core/compat/hash.php';
 include_once SYSTEM_PATH.'core/compat/password.php';
 
 include_once $dir.'/mocks/autoloader.php';
diff --git a/tests/codeigniter/core/compat/hash_test.php b/tests/codeigniter/core/compat/hash_test.php
new file mode 100644
index 0000000..25bbd4e
--- /dev/null
+++ b/tests/codeigniter/core/compat/hash_test.php
@@ -0,0 +1,51 @@
+<?php
+
+class hash_test extends CI_TestCase {
+
+	public function test_bootstrap()
+	{
+		if (is_php('5.5'))
+		{
+			return $this->markTestSkipped('ext/standard/password is available on PHP 5.5');
+		}
+
+		$this->assertTrue(function_exists('hash_pbkdf2'));
+	}
+
+	// ------------------------------------------------------------------------
+
+	/**
+	 * hash_pbkdf2() test
+	 *
+	 * Borrowed from PHP's own tests
+	 *
+	 * @depends	test_bootstrap
+	 */
+	public function test_hash_pbkdf2()
+	{
+		$this->assertEquals('0c60c80f961f0e71f3a9', hash_pbkdf2('sha1', 'password', 'salt', 1, 20));
+		$this->assertEquals(
+			"\x0c\x60\xc8\x0f\x96\x1f\x0e\x71\xf3\xa9\xb5\x24\xaf\x60\x12\x06\x2f\xe0\x37\xa6",
+			hash_pbkdf2('sha1', 'password', 'salt', 1, 20, TRUE)
+		);
+		$this->assertEquals('3d2eec4fe41c849b80c8d8366', hash_pbkdf2('sha1', 'passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt', 4096, 25));
+		$this->assertEquals(
+			"\x3d\x2e\xec\x4f\xe4\x1c\x84\x9b\x80\xc8\xd8\x36\x62\xc0\xe4\x4a\x8b\x29\x1a\x96\x4c\xf2\xf0\x70\x38",
+			hash_pbkdf2('sha1', 'passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt', 4096, 25, TRUE)
+		);
+		$this->assertEquals('120fb6cffcf8b32c43e7', hash_pbkdf2('sha256', 'password', 'salt', 1, 20));
+		$this->assertEquals(
+			"\x12\x0f\xb6\xcf\xfc\xf8\xb3\x2c\x43\xe7\x22\x52\x56\xc4\xf8\x37\xa8\x65\x48\xc9",
+			hash_pbkdf2('sha256', 'password', 'salt', 1, 20, TRUE)
+		);
+		$this->assertEquals(
+			'348c89dbcbd32b2f32d814b8116e84cf2b17347e',
+			hash_pbkdf2('sha256', 'passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt', 4096, 40)
+		);
+		$this->assertEquals(
+			"\x34\x8c\x89\xdb\xcb\xd3\x2b\x2f\x32\xd8\x14\xb8\x11\x6e\x84\xcf\x2b\x17\x34\x7e\xbc\x18\x00\x18\x1c\x4e\x2a\x1f\xb8\xdd\x53\xe1\xc6\x35\x51\x8c\x7d\xac\x47\xe9",
+			hash_pbkdf2('sha256', 'passwordPASSWORDpassword', 'saltSALTsaltSALTsaltSALTsaltSALTsalt', 4096, 40, TRUE)
+		);
+	}
+
+}
\ No newline at end of file
diff --git a/user_guide_src/source/changelog.rst b/user_guide_src/source/changelog.rst
index 9907e97..5d5c5df 100644
--- a/user_guide_src/source/changelog.rst
+++ b/user_guide_src/source/changelog.rst
@@ -508,7 +508,7 @@
       -  Changed method ``clean_string()`` to utilize ``mb_convert_encoding()`` if it is available but ``iconv()`` is not.
       -  Renamed method ``_is_ascii()`` to ``is_ascii()`` and made it public.
 
-   -  Added `compatibility layers <general/compatibility_functions>` for PHP's `mbstring <http://php.net/mbstring>`_ (limited support) and `password <http://php.net/password>`_ extensions.
+   -  Added `compatibility layers <general/compatibility_functions>` for PHP's `mbstring <http://php.net/mbstring>`_ (limited support), `hash <http://php.net/hash>`_ and `password <http://php.net/password>`_ extensions.
    -  Removed ``CI_CORE`` boolean constant from *CodeIgniter.php* (no longer Reactor and Core versions).
    -  Log Library will now try to create the **log_path** directory if it doesn't exist.
    -  Added support for HTTP-Only cookies with new config option *cookie_httponly* (default FALSE).
diff --git a/user_guide_src/source/general/compatibility_functions.rst b/user_guide_src/source/general/compatibility_functions.rst
index e025d2a..3495101 100644
--- a/user_guide_src/source/general/compatibility_functions.rst
+++ b/user_guide_src/source/general/compatibility_functions.rst
@@ -93,6 +93,35 @@
 	For more information, please refer to the `PHP manual for
 	password_verify() <http://php.net/password_verify>`_.
 
+*********************
+Hash (Message Digest)
+*********************
+
+This compatibility layer contains only a single function at
+this time - ``hash_pbkdf2()``, which otherwise requires PHP 5.5.
+
+Dependancies
+============
+
+- None
+
+Function reference
+==================
+
+.. function:: hash_pbkdf2($algo, $password, $salt, $iterations[, $length = 0[, $raw_output = FALSE]])
+
+	:param	string	$algo: Hashing algorithm
+	:param	string	$password: Password
+	:param	string	$salt: Hash salt
+	:param	int	$iterations: Number of iterations to perform during derivation
+	:param	int	$length: Output string length
+	:param	bool	$raw_output: Whether to return raw binary data
+	:returns:	Password-derived key or FALSE on failure
+	:rtype:	string
+
+	For more information, please refer to the `PHP manual for
+	hash_pbkdf2() <http://php.net/hash_pbkdf2>`_.
+
 ****************
 Multibyte String
 ****************