非常重要的通知,如果您将数组传递给 $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 "hashes match!";
}
?>
在实现 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
函数 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);
如果 (strlen($key) > $size) {
$key = str_pad(pack($pack, $algo($key)), $size, chr(0x00));
} 否则 {
$key = str_pad($key, $size, chr(0x00));
}
对于 ($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)));
返回 ($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
*/
函数 oauth_totp($key, $time, $digits=8, $crypto='sha256')
{
$digits = intval($digits);
$result = null;
// 将计数器转换为二进制(64 位)
$data = pack('NNC*', $time >> 32, $time & 0xFFFFFFFF);
// 填充到 8 个字符(如果需要)
如果 (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);
返回 $result;
}
?>
这是一个高效的 PBDKF2 实现
<?php
/*
* RSA 的 PKCS #5 定义的 PBKDF2 密钥派生函数:https://www.ietf.org/rfc/rfc2898.txt
* $algorithm - 要使用的哈希算法。推荐:SHA256
* $password - 密码。
* $salt - 密码的唯一盐。
* $count - 迭代次数。次数越高越好,但速度越慢。推荐:至少 1024 次。
* $key_length - 派生密钥的字节长度。
* $raw_output - 如果为 true,则密钥以原始二进制格式返回。否则为十六进制编码。
* 返回值:从密码和盐派生的 $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));
?>
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));
}
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);
}
?>
对于那些确实需要在 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));
}
?>
为了签署 Amazon AWS 查询,请对二进制值进行 Base64 编码
<?php
echo base64_encode(hash_hmac("sha1", $Request, $AmazonSecretKey, true));
?>
上面的 hotp 算法适用于小于 256 的计数器值,但由于计数器可能更大,因此需要遍历计数器的所有字节。
<?php
function oath_hotp ($key, $counter)
{
// 计数器
// 计数器值可能超过一个字节,所以我们需要多次循环
$cur_counter = array(0,0,0,0,0,0,0,0);
for($i=7;$i>=0;$i--)
{
$cur_counter[$i] = pack ('C*', $counter);
$counter = $counter >> 8;
}
$bin_counter = implode($cur_counter);
// 填充到 8 个字符
if (strlen ($bin_counter) < 8)
{
$bin_counter = str_repeat (chr(0), 8 - strlen ($bin_counter)) . $bin_counter;
}
// HMAC
$hash = hash_hmac ('sha1', $bin_counter, $key);
return $hash;
}
function oath_truncate($hash, $length = 6)
{
// 转换为十进制
foreach(str_split($hash,2) as $hex)
{
$hmac_result[]=hexdec($hex);
}
// 查找偏移量
$offset = $hmac_result[19] & 0xf;
// 算法来自 RFC
return
(
(($hmac_result[$offset+0] & 0x7f) << 24 ) |
(($hmac_result[$offset+1] & 0xff) << 16 ) |
(($hmac_result[$offset+2] & 0xff) << 8 ) |
($hmac_result[$offset+3] & 0xff)
) % pow(10,$length);
}
print "<pre>";
print "Compare results with:";
print " http://tools.ietf.org/html/draft-mraihi-oath-hmac-otp-04\n";
print "Count\tHash\t\t\t\t\t\tPin\n";
for($i=0;$i<=1024;$i=$i+128)
{
print $i."\t".($a=oath_hotp("12345678901234567890",$i));
print "\t".oath_truncate($a)."\n";
}
?>
<?php
/* 以下提供了一种解决方案,用于解决在 PHP 5.1 - 5.3 中使用 Tiger 算法的 hash_hmac 函数,
并在升级到 PHP 5.4(或更高版本)后遇到的问题。
问题在于,从 PHP 5.4.0 开始,Tiger 算法的字节顺序被更改为大端序。
以下两个函数断言 $algo 是 Tiger 算法之一,例如 tiger160,4。
*/
# 替换 hash('tiger...
function hash_tiger_rev($algo, $data, $raw_output = false) {
$len = intval(substr($algo, 5, 3)); # 128, 160 或 192 位
$times = substr($algo, 9, 1); # 3 或 4
$revhash = implode("", array_map("strrev",
str_split(hash('tiger192,'.$times, $data, true), 8)));
if ($len < 192) $revhash = substr($revhash, 0, $len >> 3);
return $raw_output? $revhash: bin2hex($revhash);
}
# 替换 hash_hmac('tiger...
function hash_hmac_tiger_rev($algo, $data, $key, $raw_output = false) {
if (strlen($key) > 64) $key = hash_tiger_rev($algo, $key);
$key = str_pad($key, 64, chr(0));
$o_pad = str_repeat("\\", 64) ^ $key; # "\" = chr(0x5C)
$i_pad = str_repeat("6", 64) ^ $key; # "6" = chr(0x36)
return hash_tiger_rev($algo, $o_pad .
hash_tiger_rev($algo, $i_pad . $data, true), $raw_output);
}
# 始终使用新的 Tiger 版本
function hash_hmac_new($algo, $data, $key, $raw_output = false) {
if (phpversion() > '5.4' || !preg_match('/^tiger(128|160|192),(3|4)$/', $algo))
return hash_hmac($algo, $data, $key, $raw_output);
else
return hash_hmac_tiger_rev($algo, $data, $key, $raw_output);
}
# 始终使用旧的 Tiger 版本
function hash_hmac_old($algo, $data, $key, $raw_output = false) {
if (phpversion() < '5.4' || !preg_match('/^tiger(128|160|192),(3|4)$/', $algo))
return hash_hmac($algo, $data, $key, $raw_output);
else
return hash_hmac_tiger_rev($algo, $data, $key, $raw_output);
}
# 进行测试
$algo = 'tiger160,4'; $pwd = 'foo'; $key = 'bar';
echo hash_hmac($algo, $pwd, $key), "<br>";
echo hash_hmac_tiger_rev($algo, $pwd, $key), "<br>";
echo "<br>";
echo hash_hmac_old($algo, $pwd, $key), "<br>";
echo hash_hmac_new($algo, $pwd, $key), "<br>";
/* 使用 PHP 5.4 的输出将是
590546d9f425188da35e5dfa53306ba3953571cc
bd6664330ed96b9b39ee063241b62e43f546a49d
bd6664330ed96b9b39ee063241b62e43f546a49d
590546d9f425188da35e5dfa53306ba3953571cc
使用 PHP 5.3 的输出将是
bd6664330ed96b9b39ee063241b62e43f546a49d
590546d9f425188da35e5dfa53306ba3953571cc
bd6664330ed96b9b39ee063241b62e43f546a49d
590546d9f425188da35e5dfa53306ba3953571cc
*/
?>
这是对您的数据进行哈希处理最安全的方式,
几乎不可能检索您的数据。
--------------------------------------------------------
--- 创建两个随机密钥并将其保存在您的配置文件中 ---
<?php
// 创建一个随机密钥
echo base64_encode(openssl_random_pseudo_bytes(64));
?>
--------------------------------------------------------
<?php
// 将密钥保存在您的配置文件中
define('FIRSTKEY','TNYazlbZ1Mq3HDMiEFDLrRMZBftFqpU2Ipytgytsc+jmQysE8lmigKtmGK+exB337ZOcAgwPpWmoPHL5niO3jA==');
define('SECONDKEY','z5hh/Kax4+HKZ8exOlvGlrHev/6ZynOEn904yiiIcWo/qLXWSfLkzm4NSJiGXu4uR7xxUowOkO26VqAi2p2DYQ==');
?>
--------------------------------------------------------
<?php
function secured_hash($data)
{
$first_key = base64_decode(FIRSTKEY);
$second_key = base64_decode(SECONDKEY);
$first_hashed = hash_hmac('sha3-512', $data, $first_key, TRUE);
$second_hashed = hash_hmac('sha3-512', $first_hashed, $second_key);
return $second_hashed;
}
?>
在 PHP 中生成符合 OATH 标准的 OTP(一次性密码)
<?php
$otp = oath_truncate (oath_hotp ($key, $counter), $length);
function oath_hotp ($key, $counter) {
// 计数器
$bin_counter = pack ('C*', $counter);
// 填充到 8 个字符
if (strlen ($bin_counter) < 8) {
$bin_counter = str_repeat (chr(0), 8 - strlen ($bin_counter)) . $bin_counter;
}
// HMAC
$hash = hash_hmac ('sha1', $bin_counter, $key);
return $hash;
}
function oath_truncate ($hash, $length = 6) {
// 最后一个字节用作偏移量
$offset = hexdec (substr ($hash, 38)) & 0xf;
// 提取相关部分,并清除第一个位
$hex_truncated = substr ($hash, $offset * 2, 8);
$bin_truncated = decbin (hexdec ($hex_truncated));
$bin_truncated[0] = '0';
$dec_truncated = bindec ($bin_truncated);
return substr ($dec_truncated, 0 - $length);
}
?>
根据 RCF http://tools.ietf.org/html/draft-mraihi-oath-hmac-otp-04 工作的 HOTP 算法
RCF 文档中的测试用例将 ASCII 字符串定义为“123456787901234567890”。
但十六进制解码后的字符串为“12345678901234567890”。
Secret="12345678901234567890";
计数
0 755224
1 287082
<?php
function oath_hotp($key,$counter) {
// 转换为填充的二进制字符串
$data = pack ('C*', $counter);
$data = str_pad($data,8,chr(0),STR_PAD_LEFT);
// HMAC
return hash_hmac('sha1',$data,$key);
}
function oath_truncate($hash, $length = 6) {
// 转换为十进制
foreach(str_split($hash,2) as $hex) {
$hmac_result[]=hexdec($hex);
}
// 查找偏移量
$offset = $hmac_result[19] & 0xf;
// 来自 RFC 的算法
return (
(($hmac_result[$offset+0] & 0x7f) << 24 ) |
(($hmac_result[$offset+1] & 0xff) << 16 ) |
(($hmac_result[$offset+2] & 0xff) << 8 ) |
($hmac_result[$offset+3] & 0xff)
) % pow(10,$length);
}
print "<pre>";
print "与以下结果进行比较:"
print " http://tools.ietf.org/html/draft-mraihi-oath-hmac-otp-04\n";
print "计数\t哈希值\t\t\t\t\t\tPin\n";
for($i=0;$i<10;$i++)
print $i."\t".($a=oath_hotp("12345678901234567890",$i))
print "\t".oath_truncate($a)."\n";
另一个 OATH HOTP 函数。拥有 64 位计数器,并且代码更短。请享用。
<?php
function oath_hotp ($secret, $ctr, $len=6) {
$binctr = pack ('NNC*', $ctr>>32, $ctr & 0xFFFFFFFF);
$hash = hash_hmac ("sha1", $binctr, $secret);
// 这是哈希结束和截断开始的地方
$ofs = 2*hexdec (substr ($hash, 39, 1));
$int = hexdec (substr ($hash, $ofs, 8)) & 0x7FFFFFFF;
$pin = substr ($int, -$len);
$pin = str_pad ($pin, $len, "0", STR_PAD_LEFT);
return $pin;
}
?>