深入理解 `wp_hash_password()` 函数的源码,它是如何使用 `phpass` 库对密码进行加密的?

好嘞!各位观众老爷,晚上好!今天咱们来聊聊 WordPress 密码加密的那些事儿,保证让大家听得明白,看得过瘾!

开场白:密码安全,马虎不得!

话说网络世界,信息安全那是重中之重。而密码,作为保护我们账号的第一道防线,更是得小心呵护。想想看,如果你的密码被人轻易破解,那可就惨了,轻则账号被盗,重则财产受损。

WordPress 作为一个流行的内容管理系统,对密码安全自然非常重视。它的密码加密机制,那可是经过精心设计的。今天,咱们就来扒一扒 WordPress 中 wp_hash_password() 函数的底裤,看看它是如何使用 phpass 库来保护我们的密码的。

第一章:wp_hash_password() 函数概览

首先,咱们来认识一下今天的主角:wp_hash_password() 函数。这个函数位于 wp-includes/pluggable.php 文件中,它的主要作用就是对用户输入的密码进行加密,然后存储到数据库中。

/**
 * Hashes a password using the portable PHP password hashing framework.
 *
 * @since 2.5.0
 *
 * @param string $password The password to hash.
 * @return string The hashed 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 ) );
}

这段代码看起来是不是很简单?别急,咱们一步一步来分析。

  1. 全局变量 $wp_hasher: 这个变量用来存储 PasswordHash 类的实例。这样做的好处是,避免每次调用 wp_hash_password() 函数时都重新创建 PasswordHash 对象,从而提高性能。

  2. empty( $wp_hasher ): 判断 $wp_hasher 是否为空。如果为空,说明是第一次调用 wp_hash_password() 函数,需要先加载 class-phpass.php 文件,并创建 PasswordHash 类的实例。

  3. require_once ABSPATH . 'wp-includes/class-phpass.php': 加载 class-phpass.php 文件。这个文件包含了 phpass 库的核心代码。ABSPATH 是 WordPress 的根目录。

  4. $wp_hasher = new PasswordHash( 8, true ): 创建 PasswordHash 类的实例。PasswordHash 类是 phpass 库的核心类,用于密码的加密和验证。构造函数的两个参数分别是:

    • 8: 代表 hashing iterations 的数量。这个值越大,加密强度越高,但同时也会消耗更多的 CPU 资源。WordPress 默认值是 8
    • true: 代表 portable hashes。设置成 true 会使用 MD5 加密 salt。
  5. return $wp_hasher->HashPassword( trim( $password ) ): 调用 PasswordHash 类的 HashPassword() 方法对密码进行加密。trim( $password ) 用于去除密码两端的空格。

第二章:phpass 库的奥秘

phpass (Portable PHP password hashing framework) 是一个用于密码哈希的 PHP 库,由 Openwall Project 开发。它的主要特点是:

  • 可移植性: 可以在不同的 PHP 环境下运行,不需要依赖特定的 PHP 扩展。
  • 安全性: 使用了 bcrypt 算法(如果可用)或者 MD5 算法,并加入了 salt,可以有效防止彩虹表攻击。
  • 简单易用: 提供简单的 API,方便开发者使用。

现在,咱们深入到 class-phpass.php 文件中,看看 PasswordHash 类的代码。

<?php
#
# Portable PHP password hashing framework.
#
# Version 0.3 / genuine.
#
# Written by Solar Designer <solar at openwall.com> in 2005-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 this comment in all of your copies of the code.
#
# Please make sure that you use this code as is.  Changing it may render
# it insecure.  This comment must be preserved as well.
#

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 get_random_bytes($count)
    {
        $output = '';
        if (function_exists('openssl_random_pseudo_bytes')
        && (version_compare(PHP_VERSION, '5.3.4') >= 0 || !defined('OPENSSL_ALGO_SHA256'))) {
            $output = openssl_random_pseudo_bytes($count);
        }
        if (strlen($output) < $count) {
            $output = '';
            for ($i = 0; $i < $count; $i += 16) {
                $this->random_state = md5(microtime() . $this->random_state);
                $output .= pack('H*', md5($this->random_state));
            }
            $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$", standard Blowfish is "$2a$".
        if ($id != '$P$')
            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're kind of forced to use MD5 here since it's the only
        # cryptographic primitive available in all versions of PHP
        # currently in use.  To implement something portable,
        # we need something like this.
        if (PHP_VERSION >= '5') {
            $hash = md5($salt . $password, TRUE);
            do {
                $hash = md5($hash . $password, TRUE);
            } while (--$count);
        } else {
            $hash = pack('H*', md5($salt . $password));
            do {
                $hash = pack('H*', md5($hash . $password));
            } while (--$count);
        }

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

        return $output;
    }

    function HashPassword($password)
    {
        $random = $this->get_random_bytes(6);
        $hash = $this->crypt_private($password, $this->gensalt_private($random));
        if (strlen($hash) == 34)
            return $hash;

        # Returning '*' means probably exceeding strlen limit.
        return '*';
    }

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

        return $hash == $stored_hash;
    }
}
?>

这个类包含了几个关键的方法:

  • PasswordHash(): 构造函数,初始化一些变量,例如 itoa64iteration_count_log2portable_hashes
  • get_random_bytes(): 生成随机字节。这个函数会尝试使用 openssl_random_pseudo_bytes() 函数生成随机字节,如果失败,则使用 MD5 算法生成。
  • encode64(): 将二进制数据编码成 base64 格式的字符串。
  • gensalt_private(): 生成 salt。Salt 是一个随机字符串,用于增加密码的安全性。
  • crypt_private(): 对密码进行加密。这个函数使用 MD5 算法对密码进行多次哈希,并加入 salt。
  • HashPassword(): 对密码进行哈希,并返回哈希后的字符串。
  • CheckPassword(): 验证密码是否正确。

第三章:密码哈希流程详解

现在,咱们来详细分析一下 wp_hash_password() 函数是如何对密码进行哈希的。

  1. 生成 salt: gensalt_private() 函数会生成一个随机的 salt。Salt 的长度是 8 个字符,由 get_random_bytes() 函数生成。

    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;
    }
    • $P$:这是一个标识符,表示使用了 phpass 库进行加密。
    • $this->itoa64[min($this->iteration_count_log2 + ((PHP_VERSION >= '5') ? 5 : 3), 30)]:这一段是根据迭代次数(iteration_count_log2)确定加密强度,并从 $this->itoa64 字符串中取出一个字符。itoa64 是一个包含 64 个字符的字符串,用于 base64 编码。
    • $this->encode64($input, 6):将 6 个字节的随机数($input)进行 base64 编码,并添加到 salt 中。
  2. 密码哈希: crypt_private() 函数会使用 MD5 算法对密码进行多次哈希,并加入 salt。

    function crypt_private($password, $setting)
    {
        $output = '*0';
        if (substr($setting, 0, 2) == $output)
            $output = '*1';
    
        $id = substr($setting, 0, 3);
        # We use "$P$", standard Blowfish is "$2a$".
        if ($id != '$P$')
            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're kind of forced to use MD5 here since it's the only
        # cryptographic primitive available in all versions of PHP
        # currently in use.  To implement something portable,
        # we need something like this.
        if (PHP_VERSION >= '5') {
            $hash = md5($salt . $password, TRUE);
            do {
                $hash = md5($hash . $password, TRUE);
            } while (--$count);
        } else {
            $hash = pack('H*', md5($salt . $password));
            do {
                $hash = pack('H*', md5($hash . $password));
            } while (--$count);
        }
    
        $output = substr($setting, 0, 12);
        $output .= $this->encode64($hash, 16);
    
        return $output;
    }
    • $setting: 这个参数包含了 salt 和迭代次数的信息。
    • $count: 迭代次数,由 salt 中的信息决定。
    • 循环: 循环执行 MD5 哈希,次数由 $count 决定。每次哈希都将上次的哈希结果和密码连接起来,再次进行哈希。
    • $output: 最终的哈希结果,包含了 salt 和哈希后的密码。
  3. 返回哈希后的密码: HashPassword() 函数会返回哈希后的密码。

    function HashPassword($password)
    {
        $random = $this->get_random_bytes(6);
        $hash = $this->crypt_private($password, $this->gensalt_private($random));
        if (strlen($hash) == 34)
            return $hash;
    
        # Returning '*' means probably exceeding strlen limit.
        return '*';
    }

第四章:密码验证流程详解

当用户登录时,WordPress 会使用 wp_check_password() 函数来验证用户输入的密码是否正确。wp_check_password() 函数会调用 PasswordHash 类的 CheckPassword() 方法来进行验证。

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

    return $hash == $stored_hash;
}
  • $password: 用户输入的密码。
  • $stored_hash: 存储在数据库中的哈希密码。
  • crypt_private($password, $stored_hash): 使用用户输入的密码和存储的哈希密码中的 salt,重新计算哈希值。
  • $hash[0] == '*':如果计算出的哈希值以 * 开头,表示哈希过程中出现了错误,这时直接使用存储的哈希值进行比较。
  • return $hash == $stored_hash: 比较计算出的哈希值和存储的哈希值是否相等。如果相等,则表示密码正确。

第五章:为什么使用 phpass

你可能会问,为什么 WordPress 要使用 phpass 库来加密密码呢?原因如下:

  • 可移植性: phpass 库可以在不同的 PHP 环境下运行,不需要依赖特定的 PHP 扩展。这使得 WordPress 可以运行在各种不同的服务器上。
  • 安全性: phpass 库使用了 bcrypt 算法(如果可用)或者 MD5 算法,并加入了 salt,可以有效防止彩虹表攻击。
  • 成熟稳定: phpass 库是一个成熟稳定的库,经过了时间的考验。

第六章:安全性考量和改进空间

虽然 phpass 库在当时是一个不错的选择,但现在看来,它也存在一些局限性:

  • MD5 算法: phpass 库在 PHP 5.3 之前主要使用 MD5 算法,而 MD5 算法已经被证明是不安全的。虽然 phpass 加入了 salt,但是仍然存在被破解的风险。
  • bcrypt 支持: 虽然 phpass 库支持 bcrypt 算法,但是需要 PHP 安装了 crypt() 函数,并且操作系统支持 bcrypt 算法。这在一些老旧的服务器上可能无法实现。

为了提高密码的安全性,现在更好的选择是使用 PHP 内置的 password_hash()password_verify() 函数。这两个函数使用了 bcrypt 算法(或者 Argon2i 算法,如果可用),并且提供了更高级的安全性。

WordPress 也在不断改进密码加密机制。在 WordPress 4.4 版本中,引入了 wp_check_password_compat() 函数,用于兼容旧的密码哈希方式。在 WordPress 4.4 之后的版本中,建议使用 wp_hash_password() 函数对密码进行加密,并使用 wp_check_password() 函数进行验证。这两个函数会自动选择最安全的密码哈希算法。

第七章:总结

今天,咱们一起深入学习了 WordPress 中 wp_hash_password() 函数的源码,了解了它是如何使用 phpass 库对密码进行加密的。

总而言之,wp_hash_password() 函数通过 phpass 库,为 WordPress 用户的密码安全提供了保障。当然,随着技术的不断发展,密码安全也面临着新的挑战。WordPress 也在不断改进密码加密机制,以确保用户的密码安全。

课堂小结:

功能点 描述
加密算法 主要使用 MD5 (在 PHP 5.3 之前) 或 bcrypt (如果系统支持),并加入 Salt
Salt 随机生成的字符串,用于增加密码的安全性,防止彩虹表攻击
迭代次数 对密码进行多次哈希的次数,增加破解难度
兼容性 phpass 库具有良好的可移植性,可以在不同的 PHP 环境下运行
替代方案 现在更推荐使用 PHP 内置的 password_hash()password_verify() 函数,它们使用了更安全的 bcrypt 或 Argon2i 算法
WordPress 更新 WordPress 也在不断改进密码加密机制,建议使用 wp_hash_password()wp_check_password() 函数,它们会自动选择最安全的密码哈希算法

希望今天的讲座对大家有所帮助。记住,密码安全,马虎不得!

下次再见!

发表回复

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