【PHP】更安全的密码存储之phpass库及其在CI框架中的使用

作为php初学者,我们一般认为php中自带的MD5加密方式是很安全的,因为MD5不可逆,即攻击者不能从哈希值H(x)中推出x的值。并且MD5加密算法的碰撞几率非常低,即攻击者不能找到两个值x, x'使得H(x) = H(x')。然鹅这种加密方式也是不安全的,只要美剧出所有的常用密码做成一个索引表,就可以推出来原始密码。这张索引表也被称作“彩虹表”。所以我们要寻求一种更为安全的加密方式,即便因为某种原因数据库数据泄露,攻击者也不能通过数据库中的密文密码得到用户原始密码。对密码进行哈希最安全的方法是使用bcrypt算法,开源的phpass库以一个易于使用的类来提供该功能。

一、phpass简介

phpass(发音为“pH pass”)是一个可在PHP应用程序中使用的可移植公共域密码哈希框架 。又称便携式PHP密码哈希框架。该库中中包含一个实现PasswordHash PHP类的PHP源文件,一个演示PasswordHash类使用的小型PHP应用程序,以及便携式哈希的C重新实现(仅用于测试主要实现的正确性)。phpass官网:https://www.openwall.com/phpass/

二、phpass库的下载及使用

1.首先访问phpass官网下载phpass库,根据自己服务器环境的php版本下载相应的phpass版本(此文以php7.2版本为例),然后将PasswordHash.php文件解压到你的项目目录下。

【PHP】更安全的密码存储之phpass库及其在CI框架中的使用

 2.示例代码test.php

<?php
// 引入 phpass 库
require_once('PasswordHash.php')

// 初始化散列器为不可移植(这样更安全)
$hasher = new PasswordHash(8, false);

// 计算密码的哈希值。$hashedPassword 是一个长度为 60 个字符的字符串.
$hashedPassword = $hasher->HashPassword('123456');

// 你现在可以安全地将 $hashedPassword 保存到数据库中!

// 通过echo $hashedPassword;你会发现每次输出的加密后的值不一样,
// 所以当用户登录时不能直接用用户输入的密码加密后的值和数据库中存储的值进行比较判断。
// 需要调用CheckPassword方法比较用户输入内容(产生的哈希值)和我们之前计算出的哈希值,
// 来判断用户是否输入了正确的密码
$hasher->CheckPassword('001234', $hashedPassword);  // false

$hasher->CheckPassword('123456', $hashedPassword);  // true

三、CI框架中使用phpass库的PasswordHash类

 1.因为CI框架自定义类库命名规则的限制,需要将PasswordHash.php文件名改为Password_hash.php,然后将类名改为Password_hash(可按照CI框架创建类库命名规则自行修改),然后将该文件放到application/library目录下。

2.修改原类库中的构造方法并添加初始化方法initialize()。代码如下:

function __construct(array $params = array(8, false))
{
    $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
    $this->initialize($params);
}
public function initialize(array $params = array()){
    $iteration_count_log2 = $params[0];
    $portable_hashes = $params[1];
    if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31)
        $iteration_count_log2 = 8;
	
    $this->iteration_count_log2 = $iteration_count_log2;
    $this->portable_hashes = $portable_hashes;

    $this->random_state = microtime();
    if (function_exists('getmypid'))
        $this->random_state .= getmypid();
	
    return $this;
}

2.业务逻辑示例代码:

public function test(){
	   //载入类库
        $this->load->library('Password_hash', array(8, false));

        //进行加密
        $hashed_password = $this->password_hash->HashPassword('123456');

        echo $hashed_password; //输出加密后的数据查看
        echo '<hr>';

        // 通过比较用户输入内容(产生的哈希值)和我们之前计算出的哈希值,来判断用户是否输入了正确的密码
        var_dump($this->password_hash->CheckPassword('123456', $hashed_password)); //bool(true)
        var_dump($this->password_hash->CheckPassword('000123', $hashed_password)); //bool(false)
    }

3.修改后的Password_hash.php文件源码如下:

<?php
#
# Portable PHP password hashing framework.
#
# Version 0.5 / genuine.
#
# Written by Solar Designer <solar at openwall.com> in 2004-2006 and placed in
# the public domain.  Revised in subsequent years, still public domain.
#
# There's absolutely no warranty.
#
# The homepage URL for this framework is:
#
#	http://www.openwall.com/phpass/
#
# Please be sure to update the Version line if you edit this file in any way.
# It is suggested that you leave the main version number intact, but indicate
# your project name (after the slash) and add your own revision information.
#
# Please do not change the "private" password hashing method implemented in
# here, thereby making your hashes incompatible.  However, if you must, please
# change the hash type identifier (the "$P$") to something different.
#
# Obviously, since this code is in the public domain, the above are not
# requirements (there can be none), but merely suggestions.
#
class Password_hash {
	private $itoa64;
	private $iteration_count_log2;
	private $portable_hashes;
	private $random_state;

	function __construct(array $params = array(8, false)) {
		$this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
		$this->initialize($params);
	}
	public function initialize(array $params = array()) {
		$iteration_count_log2 = $params[0];
		$portable_hashes = $params[1];
		if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) {
			$iteration_count_log2 = 8;
		}

		$this->iteration_count_log2 = $iteration_count_log2;

		$this->portable_hashes = $portable_hashes;

		$this->random_state = microtime();
		if (function_exists('getmypid')) {
			$this->random_state .= getmypid();
		}

		return $this;
	}

	function PasswordHash($iteration_count_log2, $portable_hashes) {
		self::__construct(array($iteration_count_log2, $portable_hashes));
	}

	function get_random_bytes($count) {
		$output = '';
		if (@is_readable('/dev/urandom') &&
			($fh = @fopen('/dev/urandom', 'rb'))) {
			$output = fread($fh, $count);
			fclose($fh);
		}

		if (strlen($output) < $count) {
			$output = '';
			for ($i = 0; $i < $count; $i += 16) {
				$this->random_state =
					md5(microtime() . $this->random_state);
				$output .= md5($this->random_state, TRUE);
			}
			$output = substr($output, 0, $count);
		}

		return $output;
	}

	function encode64($input, $count) {
		$output = '';
		$i = 0;
		do {
			$value = ord($input[$i++]);
			$output .= $this->itoa64[$value & 0x3f];
			if ($i < $count) {
				$value |= ord($input[$i]) << 8;
			}

			$output .= $this->itoa64[($value >> 6) & 0x3f];
			if ($i++ >= $count) {
				break;
			}

			if ($i < $count) {
				$value |= ord($input[$i]) << 16;
			}

			$output .= $this->itoa64[($value >> 12) & 0x3f];
			if ($i++ >= $count) {
				break;
			}

			$output .= $this->itoa64[($value >> 18) & 0x3f];
		} while ($i < $count);

		return $output;
	}

	function gensalt_private($input) {
		$output = '$P$';
		$output .= $this->itoa64[min($this->iteration_count_log2 +
			((PHP_VERSION >= '5') ? 5 : 3), 30)];
		$output .= $this->encode64($input, 6);

		return $output;
	}

	function crypt_private($password, $setting) {
		$output = '*0';
		if (substr($setting, 0, 2) === $output) {
			$output = '*1';
		}

		$id = substr($setting, 0, 3);
		# We use "$P$", phpBB3 uses "$H$" for the same thing
		if ($id !== '$P$' && $id !== '$H$') {
			return $output;
		}

		$count_log2 = strpos($this->itoa64, $setting[3]);
		if ($count_log2 < 7 || $count_log2 > 30) {
			return $output;
		}

		$count = 1 << $count_log2;

		$salt = substr($setting, 4, 8);
		if (strlen($salt) !== 8) {
			return $output;
		}

		# We were kind of forced to use MD5 here since it's the only
		# cryptographic primitive that was available in all versions
		# of PHP in use.  To implement our own low-level crypto in PHP
		# would have resulted in much worse performance and
		# consequently in lower iteration counts and hashes that are
		# quicker to ***** (by non-PHP code).
		$hash = md5($salt . $password, TRUE);
		do {
			$hash = md5($hash . $password, TRUE);
		} while (--$count);

		$output = substr($setting, 0, 12);
		$output .= $this->encode64($hash, 16);

		return $output;
	}

	function gensalt_blowfish($input) {
		# This one needs to use a different order of characters and a
		# different encoding scheme from the one in encode64() above.
		# We care because the last character in our encoded string will
		# only represent 2 bits.  While two known implementations of
		# bcrypt will happily accept and correct a salt string which
		# has the 4 unused bits set to non-zero, we do not want to take
		# chances and we also do not want to waste an additional byte
		# of entropy.
		$itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';

		$output = '$2a$';
		$output .= chr(ord('0') + $this->iteration_count_log2 / 10);
		$output .= chr(ord('0') + $this->iteration_count_log2 % 10);
		$output .= '$';

		$i = 0;
		do {
			$c1 = ord($input[$i++]);
			$output .= $itoa64[$c1 >> 2];
			$c1 = ($c1 & 0x03) << 4;
			if ($i >= 16) {
				$output .= $itoa64[$c1];
				break;
			}

			$c2 = ord($input[$i++]);
			$c1 |= $c2 >> 4;
			$output .= $itoa64[$c1];
			$c1 = ($c2 & 0x0f) << 2;

			$c2 = ord($input[$i++]);
			$c1 |= $c2 >> 6;
			$output .= $itoa64[$c1];
			$output .= $itoa64[$c2 & 0x3f];
		} while (1);

		return $output;
	}

	function HashPassword($password) {
		$random = '';

		if (CRYPT_BLOWFISH === 1 && !$this->portable_hashes) {
			$random = $this->get_random_bytes(16);
			$hash =
				crypt($password, $this->gensalt_blowfish($random));
			if (strlen($hash) === 60) {
				return $hash;
			}

		}

		if (strlen($random) < 6) {
			$random = $this->get_random_bytes(6);
		}

		$hash =
		$this->crypt_private($password,
			$this->gensalt_private($random));
		if (strlen($hash) === 34) {
			return $hash;
		}

		# Returning '*' on error is safe here, but would _not_ be safe
		# in a crypt(3)-like function used _both_ for generating new
		# hashes and for validating passwords against existing hashes.
		return '*';
	}

	function CheckPassword($password, $stored_hash) {
		$hash = $this->crypt_private($password, $stored_hash);
		if ($hash[0] === '*') {
			$hash = crypt($password, $stored_hash);
		}

		# This is not constant-time.  In order to keep the code simple,
		# for timing safety we currently rely on the salts being
		# unpredictable, which they are at least in the non-fallback
		# cases (that is, when we use /dev/urandom and bcrypt).
		return $hash === $stored_hash;
	}
}