WordPress密码兼容:一次穿越时空的密码学之旅
大家好,我是你们今天的密码考古学家,咳咳,讲师。今天咱们不挖坟,挖的是WordPress密码系统的历史遗迹,看看wp_check_password()
这个函数是如何做到“老少通吃”,既能理解远古时代的密码,又能驾驭新时代的加密技术,保证用户升级体验如丝般顺滑的。
密码学:WordPress的阿喀琉斯之踵
在开始之前,我们先达成一个共识:密码安全是网站安全的基石。如果密码被破解,再精妙的设计、再强大的服务器,都如同纸糊的房子,不堪一击。WordPress深知这一点,所以在密码处理上一直小心翼翼,步步为营。
但问题来了,技术在进步,黑客也在进化。早期的密码加密算法可能在现在看来漏洞百出。为了应对这种变化,WordPress不得不升级密码加密方式。但升级就意味着兼容性问题:如果直接切换到新的加密方式,所有老用户的密码都将失效,用户体验瞬间跌入谷底,这显然是不可接受的。
所以,wp_check_password()
应运而生,它的核心任务就是:在保证安全的前提下,兼容旧版本的密码哈希算法,让用户无痛升级。
wp_check_password()
:密码界的“百变星君”
wp_check_password()
函数的源码位于wp-includes/pluggable.php
文件中。我们先来看一下它的基本结构(为了方便理解,我稍微简化了一下,去掉了部分debug代码):
function wp_check_password( $password, $hash, $user_id = '' ) {
global $wp_hasher;
/**
* Filters whether to short-circuit the password check.
*
* @since 3.5.0
*
* @param bool $pre_check Whether to short-circuit the password check. Default false.
* @param string $password The user-supplied password to check.
* @param string $hash A hash (usually retrieved from the database) to check the password against.
* @param string $user_id The user ID.
*/
$check = apply_filters( 'check_password', false, $password, $hash, $user_id );
if ( $check ) {
return apply_filters( 'check_password_result', $check, $password, $hash, $user_id );
}
if ( strpos( $hash, '$P$' ) === 0 ) {
// Old WordPress pre-2.5 hashing.
$portable_hashes = apply_filters( 'portable_hashes', true );
if ( ! $portable_hashes ) {
return false;
}
if ( ! class_exists( 'PasswordHash' ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
}
$wp_hasher = new PasswordHash( 8, true );
$check = $wp_hasher->CheckPassword( $password, $hash );
if ( $check ) {
return apply_filters( 'check_password_result', true, $password, $hash, $user_id );
}
return apply_filters( 'check_password_result', false, $password, $hash, $user_id );
}
if ( strpos( $hash, '$H$' ) === 0 ) {
// Old PHPass, pre-2.5, and not supported.
return apply_filters( 'check_password_result', false, $password, $hash, $user_id );
}
if ( empty( $wp_hasher ) ) {
require_once ABSPATH . WPINC . '/class-phpass.php';
$wp_hasher = new PasswordHash( 8, true );
}
$check = $wp_hasher->CheckPassword( $password, $hash );
if ( $check ) {
return apply_filters( 'check_password_result', true, $password, $hash, $user_id );
}
return apply_filters( 'check_password_result', false, $password, $hash, $user_id );
}
这个函数接收三个参数:
$password
: 用户输入的明文密码。$hash
: 存储在数据库中的密码哈希值。$user_id
: 用户ID(可选,用于某些插件)。
接下来,我们逐行分析这个函数的工作流程:
-
过滤器(Filter)优先:
$check = apply_filters( 'check_password', false, $password, $hash, $user_id ); if ( $check ) { return apply_filters( 'check_password_result', $check, $password, $hash, $user_id ); }
WordPress的强大之处在于其插件机制。这里首先通过
apply_filters('check_password', ...)
,允许其他插件“劫持”密码验证过程。如果某个插件返回了true
或false
,表示它已经完成了验证,wp_check_password()
会直接返回插件的结果。这为开发者提供了极大的灵活性,例如可以使用外部认证服务,或者实现自定义的密码策略。 -
识别远古时代的密码(
$P$
):if ( strpos( $hash, '$P$' ) === 0 ) { // Old WordPress pre-2.5 hashing. $portable_hashes = apply_filters( 'portable_hashes', true ); if ( ! $portable_hashes ) { return false; } if ( ! class_exists( 'PasswordHash' ) ) { require_once ABSPATH . WPINC . '/class-phpass.php'; } $wp_hasher = new PasswordHash( 8, true ); $check = $wp_hasher->CheckPassword( $password, $hash ); if ( $check ) { return apply_filters( 'check_password_result', true, $password, $hash, $user_id ); } return apply_filters( 'check_password_result', false, $password, $hash, $user_id ); }
这里是关键所在。WordPress 2.5之前的版本使用了一种非常古老的、基于DES的哈希算法,其哈希值以
$P$
开头。这段代码检测到这种情况后,会加载PasswordHash
类(位于wp-includes/class-phpass.php
),并使用它来验证密码。-
$portable_hashes
过滤器: 这是一个安全开关。 早期DES实现因性能问题,依赖于特定硬件。portable_hashes
过滤器允许管理员禁用对这些旧哈希的支持,以防止潜在的安全风险。 如果禁用,那么包含$P$
的哈希将直接验证失败。 -
PasswordHash
类: 这个类负责处理旧的密码哈希。 它包含了必要的逻辑来解密和验证这些哈希。
-
-
识别更远古的密码(
$H$
):if ( strpos( $hash, '$H$' ) === 0 ) { // Old PHPass, pre-2.5, and not supported. return apply_filters( 'check_password_result', false, $password, $hash, $user_id ); }
比
$P$
更古老的哈希以$H$
开头。由于安全性太差,WordPress直接放弃了对这种哈希的支持,直接返回false
,拒绝验证。这意味着使用这种密码的用户必须重置密码。 -
现代密码验证:
if ( empty( $wp_hasher ) ) { require_once ABSPATH . WPINC . '/class-phpass.php'; $wp_hasher = new PasswordHash( 8, true ); } $check = $wp_hasher->CheckPassword( $password, $hash ); if ( $check ) { return apply_filters( 'check_password_result', true, $password, $hash, $user_id ); } return apply_filters( 'check_password_result', false, $password, $hash, $user_id );
如果密码哈希既不是
$P$
也不是$H$
,那么就认为它是使用较新的bcrypt算法生成的。这段代码会再次确保PasswordHash
类被加载,并使用它来验证密码。 bcrypt是目前公认的比较安全的密码哈希算法,WordPress从2.5版本开始使用它。
PasswordHash
类:密码加密的幕后英雄
PasswordHash
类是整个密码兼容机制的核心。它不仅负责验证旧的DES哈希,还负责验证新的bcrypt哈希。我们来看看它的关键方法:
-
PasswordHash( $iteration_count, $portable_hashes )
: 构造函数。$iteration_count
指定bcrypt算法的迭代次数,迭代次数越多,破解难度越大,但验证时间也会相应增加。$portable_hashes
参数用于控制是否生成旧的DES哈希。 -
HashPassword( $password )
: 根据当前配置生成密码哈希值。 -
CheckPassword( $password, $hash )
: 验证密码是否与给定的哈希值匹配。
<?php
// class-phpass.php (简化版)
class PasswordHash {
var $itoa64;
var $iteration_count;
var $portable_hashes;
var $random_state;
function PasswordHash( $iteration_count, $portable_hashes ) {
$this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
if ( $iteration_count < 4 )
$iteration_count = 4;
if ( $iteration_count > 31 )
$iteration_count = 31;
$this->iteration_count = $iteration_count;
$this->portable_hashes = $portable_hashes;
$this->random_state = microtime();
if ( function_exists( 'getmypid' ) )
$this->random_state .= getmypid();
}
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 .= 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 + CRYPT_SALT_LENGTH, 54 ) ];
$output .= $this->encode64( $input, CRYPT_SALT_LENGTH );
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$", phpBB's "$H$" means MD5...
if ( $id != '$P$' )
return $output;
$count_log2 = strpos( $this->itoa64, $setting[3] );
if ( $count_log2 < 7 || $count_log2 > 30 )
return $output;
$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 lookalike PBKDF2
# would require mcrypt, which is sadly not guaranteed.
$count = 1 << $count_log2;
do {
$password = md5( $salt . $password, true );
} while ( --$count );
$output = substr( $setting, 0, 12 );
$output .= $this->encode64( $password, 16 );
return $output;
}
function HashPassword( $password ) {
$random = $this->get_random_bytes(CRYPT_SALT_LENGTH);
$hash = $this->crypt_private( $password, $this->gensalt_private( $random ) );
if ( strlen( $hash ) == 34 )
return $hash;
# Returning '*' also means an error in crypt().
# However, we rely on it in wp_check_password() to
# recognize correct password hashes.
return md5( $password );
}
function CheckPassword( $password, $hash ) {
if ( substr( $hash, 0, 2 ) == '*0' )
return false;
if ( substr( $hash, 0, 2 ) == '*1' )
return false;
$crypt = $this->crypt_private( $password, $hash );
if ( $crypt == $hash )
return true;
# If the hash is in the old format, update it.
if ( substr( $hash, 0, 3 ) == '$P$' )
return true;
return false;
}
}
?>
让我们深入了解一下PasswordHash
类中的关键方法:
-
crypt_private($password, $setting)
:DES哈希的核心这个方法是实现旧的DES哈希算法的关键。它接收明文密码和哈希设置(包含盐值和迭代次数),然后使用MD5进行多次哈希迭代。
- 迭代次数:
$count_log2
变量从哈希设置中提取迭代次数的对数。实际迭代次数是1 << $count_log2
。 这意味着迭代次数是2的幂,例如,如果count_log2
是8,那么迭代次数就是256。 - 盐值:
$salt
是从哈希设置中提取的8个字符的盐值。盐值用于增加密码的安全性,防止彩虹表攻击。 - MD5迭代: 密码和盐值会被拼接起来,然后使用MD5进行哈希。这个过程会重复多次,次数由迭代次数决定。
- 迭代次数:
-
HashPassword($password)
:生成新的哈希这个方法用于生成新的密码哈希。它首先生成一个随机盐值,然后调用
crypt_private()
方法来生成哈希。 需要注意的是,即使在PasswordHash
类中,生成新哈希仍然使用MD5,这主要是为了兼容旧系统。更现代的WordPress版本使用更安全的bcrypt算法。 -
CheckPassword($password, $hash)
:验证密码这个方法用于验证密码是否与给定的哈希匹配。它首先检查哈希是否是已知的不安全哈希(以
*0
或*1
开头),如果是,则直接返回false
。然后,它调用crypt_private()
方法来计算密码的哈希,并将其与给定的哈希进行比较。如果匹配,则返回true
。 重点: 如果哈希是以$P$
开头的旧格式,即使密码匹配,该方法也会返回true
。 这是因为wp_check_password()
函数会在验证成功后更新哈希到更安全的格式。
平滑升级的策略
现在,我们知道了wp_check_password()
如何识别和验证不同版本的密码哈希。那么,它是如何实现平滑升级的呢?
答案就在于:验证成功后,立即更新哈希值。
当wp_check_password()
成功验证了一个旧的$P$
哈希时,它会返回true
。然后,WordPress会在后台使用新的bcrypt算法重新哈希密码,并更新数据库中的哈希值。这样,用户下次登录时,就会使用新的哈希值进行验证,而无需手动重置密码。
这种策略被称为“lazy migration”(懒惰迁移)或“just-in-time migration”(即时迁移)。它的优点是:
- 对用户无感知: 用户无需任何操作即可完成密码升级。
- 逐步迁移: 只有在用户登录时才会更新密码,避免了大规模的数据迁移,降低了服务器的压力。
- 兼容性强: 可以兼容各种旧版本的密码哈希。
新时代的密码安全
虽然wp_check_password()
在兼容性方面做得很好,但随着技术的发展,我们需要更安全的密码哈希算法。WordPress在后续版本中引入了wp_hash_password()
和wp_verify_password()
这两个函数,它们使用了更强大的bcrypt算法,并提供了更多的安全选项。
wp_hash_password( $password )
: 使用bcrypt算法生成密码哈希值。wp_verify_password( $password, $hash )
: 验证密码是否与给定的bcrypt哈希值匹配。
这些函数的使用,标志着WordPress密码安全进入了一个新的时代。
总结
wp_check_password()
函数是WordPress密码兼容机制的核心。它通过识别不同版本的密码哈希,并采用“lazy migration”策略,实现了平滑升级,保证了用户体验。
以下表格总结了WordPress密码哈希的发展历程:
版本 | 哈希算法 | 哈希值前缀 | 安全性 | 是否支持 | 备注 |
---|---|---|---|---|---|
WordPress 2.5之前 | DES | $P$ |
低 | 支持 | 使用基于DES的哈希算法,安全性很低。 为了兼容性仍然支持,但验证后会立即更新为bcrypt。 |
WordPress 2.5之前 | MD5 | $H$ |
非常低 | 不支持 | 使用MD5哈希算法,安全性极低。 WordPress不再支持,使用这种密码的用户必须重置密码。 |
WordPress 2.5之后 | bcrypt | 无固定前缀 | 高 | 支持 | 使用bcrypt算法,安全性较高。 是目前推荐的密码哈希算法。 |
通过对wp_check_password()
函数的分析,我们可以看到WordPress在密码安全方面所做的努力。它不仅要保证网站的安全,还要兼顾用户体验,这是一个非常具有挑战性的任务。
希望今天的讲座能让大家对WordPress密码兼容机制有一个更深入的了解。谢谢大家!