非常重要的注意事項,如果您將陣列傳遞給 $data,PHP 將會產生警告,返回 NULL 並繼續您的應用程式。我認為這是一個嚴重的漏洞,因為這個函數通常用於檢查授權。
範例
<?php
var_dump(hash_hmac('sha256', [], 'secret'));
警告:hash_hmac() 預期參數 2 為字串,給定陣列,在第 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'));
警告:hash_hmac() 預期參數 2 為字串,給定陣列,在第 3 行
NULL
?>
當然,這不是文件中記載的功能。
比較雜湊值時請務必小心。在某些情況下,使用計時攻擊可能會洩漏資訊。這是利用 == 運算子只比較到發現兩個字串之間存在差異為止的特性。為了防止這種情況,您有兩個選項。
選項 1:先將兩個雜湊值再次雜湊 - 這不會阻止計時差異,但會使洩漏的資訊變得無用。
<?php
if (md5($hashed_value) === md5($hashed_expected)) {
echo "雜湊值相符!";
}
?>
選項 2:總是比較整個字串。
<?php
if (hash_compare($hashed_value, $hashed_expected)) {
echo "雜湊值相符!";
}
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 擴充功能的存取權限。如果您需要 HMAC 產生器,但 Hash 功能無法使用,這裡提供一個 hash_hmac 函式的複製版本。它僅適用於 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 key derivation function as defined by RSA's PKCS #5: https://www.ietf.org/rfc/rfc2898.txt
* $algorithm - The hash algorithm to use. Recommended: SHA256
* $password - The password.
* $salt - A salt that is unique to the password.
* $count - Iteration count. Higher is better, but slower. Recommended: At least 1024.
* $key_length - The length of the derived key in bytes.
* $raw_output - If true, the key is returned in raw binary format. Hex encoded otherwise.
* Returns: A $key_length-byte key derived from the password and salt.
*
* Test vectors can be found here: https://www.ietf.org/rfc/rfc6070.txt
*
* This implementation of PBKDF2 was originally created by defuse.ca
* With improvements by 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 ERROR: Invalid hash algorithm.');
if($count <= 0 || $key_length <= 0)
die('PBKDF2 ERROR: Invalid parameters.');
$hash_length = strlen(hash($algorithm, "", true));
$block_count = ceil($key_length / $hash_length);
$output = "";
for($i = 1; $i <= $block_count; $i++) {
// $i encoded as 4 bytes, big endian.
$last = $salt . pack("N", $i);
// first iteration
$last = $xorsum = hash_hmac($algorithm, $last, $password, true);
// perform the other $count - 1 iterations
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 = $st+$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++)
{
// 對每次迭代結果進行 XOR 運算
$ib ^= ($h = hash_hmac($a, $h, $p, true));
}
$dk .= $ib; // 附加迭代後的區塊
}
// 返回正確長度的衍生金鑰
return substr($dk, $st, $kl);
}
?>
hmac sha1 的簡單實作
<?php
函式 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 進行 XOR 運算
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
函數 hash_hmac_crc32(字串 $key, 字串 $data): 字串
{
$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));
}
?>