分析 WordPress `wp_hash_password()` 函数的源码:如何利用 `phpass` 库实现安全的密码哈希。

各位代码界的“靓仔靓女”们,晚上好!欢迎来到今晚的密码安全“脱口秀”!今天咱们要扒的是WordPress里负责“守护贞操”(保护密码安全)的wp_hash_password() 函数,看看它到底是怎么利用phpass 这个“秘密武器”来对我们的密码进行加密的。

开场白:密码的世界,水很深!

在互联网的世界里,密码的重要性不言而喻。想象一下,你的银行账户、邮箱、社交媒体账号,甚至你的WordPress网站,都依赖于一串你自认为足够复杂的字符。但是,直接存储用户的原始密码简直就是裸奔,一旦数据库泄露,所有用户的信息都会暴露无遗。

所以,我们需要一种方式,把密码“变丑”,让别人即使拿到这串“丑密码”,也无法轻易还原成原始密码。 这就是密码哈希的目的!

第一幕:wp_hash_password() 函数登场!

wp_hash_password()函数是WordPress中用于对用户密码进行哈希处理的关键函数。简而言之,它接收一个明文密码作为输入,然后使用加密算法将其转换为一个难以破解的哈希值,并将其存储在数据库中。

我们来看看wp_hash_password()函数的真面目(简化版):

function wp_hash_password( $password ) {
    global $wp_hasher;

    if ( empty( $wp_hasher ) ) {
        require_once ABSPATH . 'wp-includes/class-phpass.php';
        $wp_hasher = new PasswordHash( 8, true );
    }

    return $wp_hasher->HashPassword( trim( $password ) );
}

哇哦!代码很简单嘛!但是别被表面现象迷惑了,精彩的都在背后呢。

  • global $wp_hasher;: 这行代码声明了一个全局变量$wp_hasher。 这个变量将用于存储一个PasswordHash 类的实例,这个类才是真正干活的。
  • if ( empty( $wp_hasher ) ) { ... }: 这部分代码检查$wp_hasher 是否为空。如果是空的,就表示我们还没有创建PasswordHash类的实例。
  • require_once ABSPATH . 'wp-includes/class-phpass.php';: 如果$wp_hasher 为空,这行代码会包含class-phpass.php 文件。这个文件是phpass 库的核心,它包含了用于密码哈希的PasswordHash 类。ABSPATH 是WordPress 常量,代表WordPress 的根目录。
  • $wp_hasher = new PasswordHash( 8, true );: 这行代码创建了PasswordHash 类的一个新实例,并将其赋值给$wp_hasherPasswordHash 类的构造函数接受两个参数:
    • 8: 这表示哈希的迭代次数的log2值。迭代次数越高,哈希过程越慢,破解的难度也越大,但同时也会增加服务器的负担。
    • true: 这表示启用Portable哈希。Portable哈希意味着生成的哈希值可以在不同的PHP版本和服务器配置之间兼容。
  • return $wp_hasher->HashPassword( trim( $password ) );: 这行代码调用PasswordHash 类的HashPassword() 方法,将明文密码作为参数传递给它。HashPassword() 方法会对密码进行哈希处理,并返回哈希后的密码。trim()函数用于去除密码字符串开头和结尾的空白字符。

第二幕:phpass 库闪亮登场!

phpass 是一个专门用于生成和验证密码哈希的PHP库。它由Solar Designer(Openwall 项目的创始人)开发,旨在提供一种简单而安全的密码哈希解决方案。WordPress 使用 phpass 库,因为它具有以下优点:

  • 安全性高: phpass 使用bcrypt算法(或者MD5,SHA256等,但是推荐bcrypt),这是一种被广泛认可的强密码哈希算法。
  • 可移植性好: 生成的哈希值可以在不同的PHP版本和服务器配置之间兼容。
  • 易于使用: phpass 提供了简单的API,可以方便地生成和验证密码哈希。

让我们深入了解phpass 库中的PasswordHash 类,它是密码哈希的核心。

class PasswordHash {
    var $itoa64;
    var $iteration_count_log2;
    var $portable_hashes;
    var $random_state;

    function PasswordHash($iteration_count_log2, $portable_hashes) {
        $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';

        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();
    }

    function HashPassword($password) {
        $random = $this->get_random_bytes(16);
        $hash = crypt($password, $this->gensalt($random));
        if (strlen($hash) == 34)
            return $hash;

        # Returning '*' on error is safe here, but please use crypt_private()
        # in your own code.
        return '*';
    }

    function gensalt($input) {
        $output = '$P$';
        $output .= $this->itoa64[min($this->iteration_count_log2 +
            ((PHP_VERSION >= '5' && defined('CRYPT_BLOWFISH')
            && CRYPT_BLOWFISH) ? 0 : 3), 30)];
        $output .= $this->encode64($input, 16);
        return $output;
    }

    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 CheckPassword($password, $stored_hash) {
        $hash = crypt($password, $stored_hash);
        if ($hash[0] == '*')
            $hash = '*';

        return $hash == $stored_hash;
    }
}

让我们逐个分析PasswordHash 类中的关键方法:

  • __construct($iteration_count_log2, $portable_hashes): 构造函数,用于初始化类的属性。
    • $iteration_count_log2: 哈希的迭代次数的log2值。迭代次数越高,哈希过程越慢,破解的难度也越大。
    • $portable_hashes: 是否启用Portable哈希。
    • $itoa64: 一个字符串,用于将二进制数据编码为可打印的ASCII字符。
  • HashPassword($password): 对密码进行哈希处理。
    • 使用get_random_bytes() 函数生成16个随机字节作为salt。
    • 使用gensalt() 函数生成salt字符串。
    • 使用crypt() 函数对密码进行哈希处理。
    • 如果哈希成功,返回哈希后的密码,否则返回'*'
  • gensalt($input): 生成salt字符串。
    • 根据iteration_count_log2 和PHP版本生成salt字符串的前缀。
    • 使用encode64() 函数将随机字节编码为可打印的ASCII字符。
  • get_random_bytes($count): 生成指定数量的随机字节。
    • 尝试从/dev/urandom 读取随机字节。
    • 如果/dev/urandom 不可用,使用md5() 函数生成伪随机字节。
  • encode64($input, $count): 将二进制数据编码为可打印的ASCII字符。
  • CheckPassword($password, $stored_hash): 检查密码是否与存储的哈希值匹配。
    • 使用crypt() 函数对密码进行哈希处理。
    • 将哈希后的密码与存储的哈希值进行比较。
    • 如果匹配,返回true,否则返回false

第三幕: 密码哈希的原理

密码哈希的本质是将任意长度的输入(密码)转换为固定长度的输出(哈希值)。这个过程是不可逆的,也就是说,无法从哈希值反推出原始密码。

一个好的密码哈希算法应该具备以下特点:

  • 单向性: 无法从哈希值反推出原始密码。
  • 抗碰撞性: 很难找到两个不同的输入,它们产生相同的哈希值。
  • 确定性: 相同的输入始终产生相同的哈希值。
  • 快速哈希: 哈希过程应该足够快,以便在用户登录时快速验证密码。
  • 慢速破解: 即使攻击者获得了哈希值,破解密码也应该非常困难。

bcrypt 算法是目前最流行的密码哈希算法之一。它通过以下方式来增强安全性:

  • Salt: Salt是一个随机字符串,它与密码一起进行哈希处理。Salt的作用是防止彩虹表攻击。
  • Iteration: Iteration是指哈希算法重复执行的次数。Iteration次数越高,破解密码的难度就越大。

第四幕:破解密码的那些事儿

虽然密码哈希可以有效地保护密码安全,但它并不是万无一失的。攻击者可以使用各种技术来破解密码,例如:

  • 彩虹表攻击: 彩虹表是一个预先计算好的哈希值表,攻击者可以使用彩虹表来查找与给定哈希值对应的密码。
  • 暴力破解: 暴力破解是指尝试所有可能的密码组合,直到找到与给定哈希值对应的密码。
  • 字典攻击: 字典攻击是指使用一个包含常见密码的字典,尝试查找与给定哈希值对应的密码。

为了防止密码被破解,我们应该:

  • 使用强密码: 强密码应该包含大小写字母、数字和符号,并且长度应该足够长。
  • 定期更换密码: 定期更换密码可以降低密码被破解的风险。
  • 不要在多个网站上使用相同的密码: 如果一个网站的密码被破解,攻击者可以使用相同的密码来访问其他网站。
  • 启用双因素认证: 双因素认证可以增加账户的安全性。
  • 使用安全的密码哈希算法: bcrypt 算法是目前最安全的密码哈希算法之一。

第五幕:WordPress 如何验证密码?

验证密码的过程正好和哈希密码的过程相反。WordPress使用wp_check_password() 函数来验证用户输入的密码是否与存储在数据库中的哈希值匹配。

function wp_check_password( $password, $hash, $user_id = '' ) {
    global $wp_hasher;

    if ( empty( $wp_hasher ) ) {
        require_once ABSPATH . 'wp-includes/class-phpass.php';
        $wp_hasher = new PasswordHash( 8, true );
    }

    $check = $wp_hasher->CheckPassword( trim( $password ), $hash );

    /**
     * Filters whether the given password matches the hashed password.
     *
     * @since 2.5.0
     *
     * @param bool   $check    Whether the password matches the hash.
     * @param string $password The user-supplied password to check.
     * @param string $hash     A hash retrieved from the database to compare to.
     * @param string $user_id  The user ID.
     */
    return apply_filters( 'check_password', $check, $password, $hash, $user_id );
}

这个函数也用到了 phpass 库,它先是确保 $wp_hasher 对象存在,如果不存在就创建一个。然后,它调用 $wp_hasher->CheckPassword() 来比较用户输入的密码哈希值和数据库中存储的哈希值。

$wp_hasher->CheckPassword() 内部会使用相同的哈希算法和 salt 对用户输入的密码进行哈希,然后将结果与存储的哈希值进行比较。如果两者匹配,说明密码正确,否则密码错误。

总结:密码安全,任重道远!

今天我们一起深入了解了WordPress 中wp_hash_password() 函数和phpass 库,了解了密码哈希的原理,以及如何使用bcrypt 算法来保护密码安全。希望今天的“脱口秀”能帮助大家更好地理解密码安全的重要性,并采取相应的措施来保护自己的密码安全。

记住,密码安全不是一蹴而就的事情,而是一个持续的过程。我们需要不断学习新的安全知识,并及时更新我们的安全措施。

感谢大家的观看,我们下期再见! 祝大家代码无bug,生活更精彩!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注