非常重要的提示,如果您将数组传递给 $data,php 将生成警告,返回 NULL 并继续您的应用程序。我认为这是一个严重的漏洞,因为此函数通常用于检查授权。
示例
<?php
var_dump(hash_hmac('sha256', [], 'secret'));
WARNING hash_hmac() expects parameter 2 to be string, array given on line number 3
NULL
?>
当然,这并非记录在案的功能。
(PHP 5 >= 5.1.2,PHP 7,PHP 8,PECL hash >= 1.1)
hash_hmac — 使用 HMAC 方法生成带密钥的哈希值
algo
所选哈希算法的名称(例如 "sha256"
)。有关支持的算法列表,请参见 hash_hmac_algos()。
注意:
不允许使用非加密哈希函数。
data
要进行哈希处理的消息。
key
用于生成消息摘要的 HMAC 变体的共享密钥。
binary
返回一个字符串,其中包含计算出的消息摘要,以小写十六进制数字表示,除非 binary
设置为 true,在这种情况下,将返回消息摘要的原始二进制表示形式。
如果 algo
未知或是非加密哈希函数,则抛出 ValueError 异常。
版本 | 描述 |
---|---|
8.0.0 | 现在如果 algo 未知或是非加密哈希函数,则抛出 ValueError 异常;以前,返回 false 。 |
7.2.0 | 禁用非加密哈希函数(adler32、crc32、crc32b、fnv132、fnv1a32、fnv164、fnv1a64、joaat)的使用。 |
示例 #1 hash_hmac() 示例
<?php
echo hash_hmac('sha256', 'The quick brown fox jumped over the lazy dog.', 'secret');
?>
以上示例将输出
9c5c42422b03f0ee32949920649445e417b2c634050833c5165704b825c2a53b
非常重要的提示,如果您将数组传递给 $data,php 将生成警告,返回 NULL 并继续您的应用程序。我认为这是一个严重的漏洞,因为此函数通常用于检查授权。
示例
<?php
var_dump(hash_hmac('sha256', [], 'secret'));
WARNING hash_hmac() expects parameter 2 to be string, array given on line number 3
NULL
?>
当然,这并非记录在案的功能。
在比较哈希值时请注意。在某些情况下,可以通过使用计时攻击泄露信息。它利用 == 运算符仅比较到找到两个字符串之间的差异为止。为了防止这种情况,您有两个选择。
选项 1:首先对两个哈希字符串进行哈希处理 - 这不会停止计时差异,但会使信息变得无用。
<?php
if (md5($hashed_value) === md5($hashed_expected)) {
echo "hashes match!";
}
?>
选项 2:始终比较整个字符串。
<?php
if (hash_compare($hashed_value, $hashed_expected)) {
echo "hashes match!";
}
function hash_compare($a, $b) {
if (!is_string($a) || !is_string($b)) {
return false;
}
$len = strlen($a);
if ($len !== strlen($b)) {
return false;
}
$status = 0;
for ($i = 0; $i < $len; $i++) {
$status |= ord($a[$i]) ^ ord($b[$i]);
}
return $status === 0;
}
?>
正如 Michael 建议的那样,我们应该注意不要使用 ==(或 ===)来比较哈希值。从 PHP 5.6 版本开始,我们现在可以使用 hash_equals()。
因此,示例将是
<?php
if (hash_equals($hashed_expected, $hashed_value) ) {
echo "哈希值匹配!";
}
?>
在实现 TOTP 应用程序时,请注意 hash_hmac() 必须接收二进制数据,而不是十六进制字符串,才能在跨平台生成有效的 OTP。
此问题可以通过在传递给 hash_hmac() 之前将十六进制字符串转换为其二进制形式轻松解决。
<?php
$time = hex2bin('0000000003523f77'); // 时间必须采用这种“十六进制并填充”的形式
$key = hex2bin('bb57d1...'); // 160 位 = 40 位十六进制 (4 位) = 32 位 base32 (5 位)
hash_hmac('sha1', $time, $key);
?>
有时托管服务提供商不提供对 Hash 扩展的访问。以下是一个 hash_hmac 函数的克隆,您可以在需要 HMAC 生成器且 Hash 不可用时使用它。它仅适用于 MD5 和 SHA1 加密算法,但其输出与官方 hash_hmac 函数相同(至少到目前为止)。
<?php
function custom_hmac($algo, $data, $key, $raw_output = false)
{
$algo = strtolower($algo);
$pack = 'H'.strlen($algo('test'));
$size = 64;
$opad = str_repeat(chr(0x5C), $size);
$ipad = str_repeat(chr(0x36), $size);
if (strlen($key) > $size) {
$key = str_pad(pack($pack, $algo($key)), $size, chr(0x00));
} else {
$key = str_pad($key, $size, chr(0x00));
}
for ($i = 0; $i < strlen($key) - 1; $i++) {
$opad[$i] = $opad[$i] ^ $key[$i];
$ipad[$i] = $ipad[$i] ^ $key[$i];
}
$output = $algo($opad.pack($pack, $algo($ipad.$data)));
return ($raw_output) ? pack($pack, $output) : $output;
}
?>
使用示例
<?php
custom_hmac('sha1', 'Hello, world!', 'secret', true);
?>
一个实现了 RFC 6238 中概述的算法的函数 (http://tools.ietf.org/html/rfc6238)
<?php
/**
* 此函数实现了 RFC 6238 中概述的基于时间的的一次性密码算法
*
* @link http://tools.ietf.org/html/rfc6238
* @param string $key 用于 HMAC 密钥的字符串
* @param mixed $time 反映时间的值(示例中为 Unix 时间戳)
* @param int $digits 期望的 OTP 长度
* @param string $crypto 期望的 HMAC 加密算法
* @return string 生成的 OTP
*/
function oauth_totp($key, $time, $digits=8, $crypto='sha256')
{
$digits = intval($digits);
$result = null;
// 将计数器转换为二进制(64 位)
$data = pack('NNC*', $time >> 32, $time & 0xFFFFFFFF);
// 填充到 8 个字符(如有必要)
if (strlen ($data) < 8) {
$data = str_pad($data, 8, chr(0), STR_PAD_LEFT);
}
// 获取哈希值
$hash = hash_hmac($crypto, $data, $key);
// 获取偏移量
$offset = 2 * hexdec(substr($hash, strlen($hash) - 1, 1));
// 获取我们感兴趣的部分
$binary = hexdec(substr($hash, $offset, 8)) & 0x7fffffff;
// 求模
$result = $binary % pow(10, $digits);
// 填充(如有必要)
$result = str_pad($result, $digits, "0", STR_PAD_LEFT);
return $result;
}
?>
这是一个高效的 PBKDF2 实现
<?php
/*
* PBKDF2 密钥派生函数,如 RSA 的 PKCS #5 中所定义: https://www.ietf.org/rfc/rfc2898.txt
* $algorithm - 要使用的哈希算法。推荐:SHA256
* $password - 密码。
* $salt - 密码的唯一盐值。
* $count - 迭代次数。越高越好,但速度越慢。推荐:至少 1024。
* $key_length - 派生密钥的字节长度。
* $raw_output - 如果为真,则密钥以原始二进制格式返回。否则为十六进制编码。
* 返回值:从密码和盐值派生的 $key_length 字节密钥。
*
* 测试向量可以在此处找到: https://www.ietf.org/rfc/rfc6070.txt
*
* 此 PBKDF2 实现最初由 defuse.ca 创建
* 并由 variations-of-shadow.com 改进
*/
function pbkdf2($algorithm, $password, $salt, $count, $key_length, $raw_output = false)
{
$algorithm = strtolower($algorithm);
if(!in_array($algorithm, hash_algos(), true))
die('PBKDF2 错误:无效的哈希算法。');
if($count <= 0 || $key_length <= 0)
die('PBKDF2 错误:无效的参数。');
$hash_length = strlen(hash($algorithm, "", true));
$block_count = ceil($key_length / $hash_length);
$output = "";
for($i = 1; $i <= $block_count; $i++) {
// $i 编码为 4 个字节,大端序。
$last = $salt . pack("N", $i);
// 第一次迭代
$last = $xorsum = hash_hmac($algorithm, $last, $password, true);
// 执行其他 $count - 1 次迭代
for ($j = 1; $j < $count; $j++) {
$xorsum ^= ($last = hash_hmac($algorithm, $last, $password, true));
}
$output .= $xorsum;
}
if($raw_output)
return substr($output, 0, $key_length);
else
return bin2hex(substr($output, 0, $key_length));
}
?>
对于签名 Amazon AWS 查询,请使用 base64 编码二进制值
<?php
$Sig = base64_encode(hash_hmac('sha256', $Request, $AmazonSecretKey, true));
?>
RFC 2898 中描述的 PBKDF2 密钥派生函数的实现不仅可以用于获取哈希后的密钥,还可以获取特定的 IV。
要使用它,可以按如下方式使用:-
<?php
$p = str_hash_pbkdf2($pw, $salt, 10, 32, 'sha1');
$p = base64_encode($p);
$iv = str_hash_pbkdf2($pw, $salt, 10, 16, 'sha1', 32);
$iv = base64_encode($iv);
?>
该函数应为:-
<?php
// PBKDF2 实现(在 RFC 2898 中描述)
//
// @param string p 密码
// @param string s 盐值
// @param int c 迭代次数(使用 1000 或更高)
// @param int kl 派生密钥长度
// @param string a 哈希算法
// @param int st 结果的起始位置
//
// @return string 派生密钥
function str_hash_pbkdf2($p, $s, $c, $kl, $a = 'sha256', $st=0)
{
$kb = $start+$kl; // 要计算的密钥块
$dk = ''; // 派生密钥
// 创建密钥
for ($block=1; $block<=$kb; $block++)
{
// 此块的初始哈希
$ib = $h = hash_hmac($a, $s . pack('N', $block), $p, true);
// 执行块迭代
for ($i=1; $i<$c; $i++)
{
// 对每次迭代进行异或
$ib ^= ($h = hash_hmac($a, $h, $p, true));
}
$dk .= $ib; // 附加迭代后的块
}
// 返回正确长度的派生密钥
return substr($dk, $start, $kl);
}
?>
HMAC SHA1 的简单实现
<?php
function hmac_sha1($key, $data)
{
// 将密钥调整为正好 64 个字节
if (strlen($key) > 64) {
$key = str_pad(sha1($key, true), 64, chr(0));
}
if (strlen($key) < 64) {
$key = str_pad($key, 64, chr(0));
}
// 外部和内部填充
$opad = str_repeat(chr(0x5C), 64);
$ipad = str_repeat(chr(0x36), 64);
// 将密钥与 opad 和 ipad 进行异或
for ($i = 0; $i < strlen($key); $i++) {
$opad[$i] = $opad[$i] ^ $key[$i];
$ipad[$i] = $ipad[$i] ^ $key[$i];
}
return sha1($opad.sha1($ipad.$data, true));
}
适用于那些确实需要在 PHP>7.1 中使用 crc32 算法的人员的功能
<?php
function hash_hmac_crc32(string $key, string $data): string
{
$b = 4;
if (strlen($key) > $b) {
$key = pack("H*", hash('crc32', $key));
}
$key = str_pad($key, $b, chr(0x00));
$ipad = str_pad('', $b, chr(0x36));
$opad = str_pad('', $b, chr(0x5c));
$k_ipad = $key ^ $ipad;
$k_opad = $key ^ $opad;
return hash('crc32', $k_opad . hash('crc32', $k_ipad . $data, true));
}
?>