PHP 哈希运算消息认证码HMAC


HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code),HMAC运算利用哈希算法,以一个密钥和一个消息为输入,生成一个消息摘要作为输出。实际上,HMAC并不是一个用来加密的算法,应该理解是用于对数据合法性进行校验的一种方式,例如接口开发、用户授权等.


引言

PHP 中 加密扩展 有:

  • CSPRNG
  • Hash
  • Mcrypt
  • Mhash
  • OpenSSL
  • 密码散列算法
  • Sodium

HMAC 运算

HMAC是密钥相关的哈希运算消息认证码(Hash-based Message Authentication Code),HMAC运算利用哈希算法, 以一个密钥和一个消息为输入,生成一个消息摘要作为输出。 实际上,HMAC并不是一个用来加密的算法,应该理解是用于对数据合法性进行校验的一种方式,例如接口开发、用户授权等.

Hash 函数:

  • hash_algos — 返回已注册的哈希算法列表
  • hash_copy — 拷贝哈希运算上下文
  • hash_equals — 可防止时序攻击的字符串比较
  • hash_file — 给指定文件的内容生成哈希值
  • hash_final — 结束增量哈希,并且返回摘要结果
  • hash_hkdf — Generate a HKDF key derivation of a supplied key input
  • hash_hmac_algos — Return a list of registered hashing algorithms suitable for hash_hmac
  • hash_hmac_file — 使用 HMAC 方法和给定文件的内容生成带密钥的哈希值
  • hash_hmac — 使用 HMAC 方法生成带有密钥的哈希值
  • hash_init — 初始化增量哈希运算上下文
  • hash_pbkdf2 — 生成所提供密码的 PBKDF2 密钥导出
  • hash_update_file — 从文件向活跃的哈希运算上下文中填充数据
  • hash_update_stream — 从打开的流向活跃的哈希运算上下文中填充数据
  • hash_update — 向活跃的哈希运算上下文中填充数据
  • hash — 生成哈希值 (消息摘要)

md5()大家应该用的很熟悉了,一般用来储存用户登录加密密码,这样在数据库中就不会出现明文密码, 被黑客拿到数据库信息也无法逆向破解用户登录密码,这样用户的安全性得到了保证。

hash_hmac()作用一样,多用在接口开发中数据来源合法性校验,例如B服务器向A服务器传递数据,A给了B一个key,B把哈希运算后的字符串传给A, A拿到B的数据也进行哈希运算看值是否一致。看一下实例:

/* B传递数据给A逻辑 */ 
$key = '5a733d89828a70ade70e1eeabdb50180712';  // A给B的key
$arr = [
    'a' => 'A',
    'b' => 'B',
    'time' => time(),
];  // 要传递的数据
$str = json_encode( $arr );
$sign = hash_hmac('sha256', $str, $key);  // HMAC运算
$postData = array_merge( $arr, ['sign' => $sign] );  // 把sign一并传递过去
$data = json_encode( $postData );  // json格式化
// 上面是准备数据,下面传递执行
...
/* A接受到B传递的数据验证逻辑 */ 
$key = '5a733d89828a70ade70e1eeabdb50180712';  // A确定的key
$postData = json_decode( $_POST['data'], true) ;  // 获取数据
$arr = [
    'a' => $postData['a'],
    'b' => $postData['b'],
    'time' => $postData['time'],
];  // 准备数据
$str = json_encode( $arr );
$sign = hash_hmac('sha256', $str, $key);  // HMAC运算
if ( $sign == $postData['sign'] ) {
    // 验证正确,逻辑处理
    ...
} else {
    // 验证错误
    echo '非法来源';
}

hash_hmac() 函数原型:

/**
 * (PHP 5 &gt;= 5.1.2, PECL hash &gt;= 1.1)<br/>
 * Generate a keyed hash value using the HMAC method
 * @link https://php.net/manual/en/function.hash-hmac.php
 * @param string $algo <p>
 * Name of selected hashing algorithm (i.e. "md5", "sha256", "haval160,4", etc..) See <b>hash_algos</b> for a list of supported algorithms.<br/>
 * Since 7.2.0 usage of non-cryptographic hash functions (adler32, crc32, crc32b, fnv132, fnv1a32, fnv164, fnv1a64, joaat) was disabled.
 * </p>
 * @param string $data <p>
 * Message to be hashed.
 * </p>
 * @param string $key <p>
 * Shared secret key used for generating the HMAC variant of the message digest.
 * </p>
 * @param bool $binary [optional] <p>
 * When set to <b>TRUE</b>, outputs raw binary data.
 * <b>FALSE</b> outputs lowercase hexits.
 * </p>
 * @return string a string containing the calculated message digest as lowercase hexits
 * unless <i>raw_output</i> is set to true in which case the raw
 * binary representation of the message digest is returned.
 */
#[Pure]
function hash_hmac ($algo, $data, $key, $binary = false) {}

/**
 * (PHP 5 &gt;= 5.1.2, PECL hash &gt;= 1.1)<br/>
 * Generate a hash value (message digest)
 * @link https://php.net/manual/en/function.hash.php
 * @param string $algo <p>
 * Name of selected hashing algorithm (i.e. "md5", "sha256", "haval160,4", etc..)
 * </p>
 * @param string $data <p>
 * Message to be hashed.
 * </p>
 * @param bool $binary [optional] <p>
 * When set to <b>TRUE</b>, outputs raw binary data.
 * <b>FALSE</b> outputs lowercase hexits.
 * </p>
 * @return string a string containing the calculated message digest as lowercase hexits
 * unless <i>raw_output</i> is set to true in which case the raw
 * binary representation of the message digest is returned.
 */
#[Pure]
function hash ($algo, $data, $binary = false) {}

OpenSSL

OpenSSL 库来对称/非对称加解密,以及 PBKDF2、 PKCS7、 PKCS12、 X509 和其他加密操作。除此之外还提供了 TLS 流的实现。

PHP 在进入7.x 时代后,默认就不再附带 mcrypt 扩展,mcrypt 将被 openssl_* 一族函数所替代。 所以,对于 PHPer 来说,有必要学习一下 PHP 的 OpenSSL 扩展。

本文就先从 OpenSSL 扩展中的对称加密说起。后面会陆续更多非对称加密、数字签名、数字证书等函数的讲解。

PHP 的 OpenSSL 扩展中,对称加密的相关函数有:

  • openssl_encrypt()
  • openssl_decrypt()
  • openssl_random_pseudo_bytes()
  • openssl_get_cipher_methods()
  • openssl_cipher_iv_length()

光看PHP的官方文档还有点难理解。上一段代码,更清楚地看下这些函数怎么完成加密的:

// 加密算法
1. $encryptMethod = 'aes-256-cbc';
// 明文数据
2. $data = 'Hello World';

// 生成IV
3. $ivLength = openssl_cipher_iv_length($encryptMethod);
4. $iv = openssl_random_pseudo_bytes($ivLength, $isStrong);
5. if (false === $iv && false === $isStrong) {
6.     die('IV generate failed');
7. }

// 加密
8. $encrypted = openssl_encrypt($data, $encryptMethod, 'secret', 0, $iv);
// 解密
9. $decrypted = openssl_decrypt($encrypted, $encryptMethod, 'secret', 0, $iv);

详细解释一下:

第 1 行 指定了加密算法。比如这段代码使用 aes-256-cbc 算法加密。其实PHP的OpenSSL扩展支持很多种加密算法, 想知道所有对称加密算法名称列表,可以调用 openssl_get_cipher_methods() 函数,这会返回一个数组:

array(
  0 => 'AES-128-CBC',
  1 => 'AES-128-CBC-HMAC-SHA1',
  ...
  7 => 'AES-128-ECB',
  ...
  31 => 'BF-CBC',
  200 => 'seed-ofb',
)

你会发现函数返回将近200种加密算法,实际上没有这么多,许多只是因为大小写不同而重复了, 比如 AES-128-CBC 和 aes-128-cbc 实际上是同一种加密算法。如果去掉重复项, 那么 PHP 的 OpenSSL 扩展支持大概100多种不同的加密算法。

第 3 ~ 7 行 生成了 IV。为什么要生成 IV,这个 IV 有什么用?

回顾一下 openssl_get_cipher_methods() 返回的加密算法列表,有很多名字中间带有 “CBC” 字样, 这些加密算法使用了同一种加密模式,也就是 密码分组链接模式(Cipher Block Chaining)。

在 CBC 模式的加密算法中,明文会被分成若干个组,以组为单位加密。每个组的加密过程,依赖他前一个组的数据: 需要跟前一组的数据进行异或操作后生成本组的密文。那么最开头的那个组又要依赖谁呢? 依赖的就是 IV,所以这就是为什么 IV 要叫初始化向量。IV 是 初始化向量(initialization vector)的缩写

IV 应该是随机生成的,所以代码用到了 openssl_random_pseudo_bytes() 生成 IV。该函数接收一个 int,代表需要生成的 IV 的长度。

IV 长度随加密算法不同而不同。一般人是记不住那么多算法需要的 IV 长度的。所以直接使用 openssl_cipher_iv_length() 函数, 这个函数返回一个 int,表示加密算法需要的 IV 长度:

echo openssl_cipher_iv_length('AES-256-CBC'); // 16
echo openssl_cipher_iv_length('BC-CBC'); // 8
echo openssl_cipher_iv_length('AES-128-ECB'); // 0

比如 AES-256-CBC 需要16位的 IV、 BC-CBC 需要 8 位的 IV、而AES-128-ECB 不需要 IV,所以返回了 0。

第 8 ~ 9 行 是加密和解密。分别使用了 openssl_encrypt() 和 openssl_decrypt()。

  • 第一个参数是输入,对 openssl_encrypt() 来说是明文串,对 openssl_decrypt() 来说是密文串
  • 第二个参数是指定加密 / 解密 算法
  • 第三个参数是加密 / 解密时需要用到的密码,是个字符串
  • 第四个参数额外选项,没有特殊需要可以保持默认值:0,
  • 第五个参数是 IV

这两个函数除了第一个参数不同,其余参数都要保证相同才能顺利解密。最后,在使用需要 IV 的加密算法时,需要注意:

  • 必须传 $iv 参数,不传的话PHP将会抛出一个 Warning
  • IV 应该是随机生成的(比如用 openssl_random_pseudo_bytes() ),不能人为设定
  • 每次加密都应该重新生成一次 IV ,不可偷懒多次加密采用相同 IV
  • IV 要随着密文一起保存(不然就没法解密了啦),可以直接附在密文串后面,也可以分开保存

密码散列算法

密码散列算法 API 提供了 crypt() 等密码算法的简单易用包装, 以一种简洁易用安全的方式创建和管理密码。

密码散列算法函数:

  • password_algos — Get available password hashing algorithm IDs
  • password_get_info — 返回指定散列(hash)的相关信息
  • password_hash — 创建密码的散列(hash)
  • password_needs_rehash — 检测散列值是否匹配指定的选项
  • password_verify — 验证密码是否和散列值匹配

Sodium 拓展

Sodium is a modern, easy-to-use software library for encryption, decryption, signatures, password hashing and more. Its goal is to provide all of the core operations needed to build higher-level cryptographic tools.

具体可以看下下面的参考资料。

Sodium 出现的目的也是为了代替 Mcrypt 这个原来的加密扩展。在 PHP7.2 之后,Mcrypt 已经被移除, 在 PHP7.1 时就已经被标记为过时。不过,Sodium 扩展的应用也并不是很多,大部分情况下我们都会使用 OpenSSL 来进行加密操作, 同时,Sodium 扩展提供的函数也非常多,所以,我们这篇文章只做了解即可。当然,最主要的是, 关于这个扩展即使是官方文档也没有完善,大部分函数的参数说明都没有,搜索出来的资料也是非常少。

Sodium 扩展在 PHP7.2 后是跟随 PHP 源码一起发布的,只需要在编译的时候加上 --with-sodium 即可安装成功。 如果是 PHP7.2 之前的版本,需要单独安装这个扩展。同时,操作系统中也需要安装 libsodium-devel 库。

AEAD_AES_256_GCM 加解密

首先是这个 AEAD_AES_256_GCM 加解密能力函数的应用。在微信支付相关的开发中,有一个接口就是使用的这种方式进行数据加密, 在官方文档中,也提供了 PHP 对应的解密方式,其中使用的就是 Sodium 扩展库中的函数。

$data = '测试加密'; // 原始数据
$nonce = random_bytes(SODIUM_CRYPTO_AEAD_AES256GCM_NPUBBYTES); // 加密证书的随机串,加密证书的随机串
$ad = 'fullstackpm'; // 加密证书的随机串
$kengen = sodium_crypto_aead_aes256gcm_keygen(); // 密钥

// 是否可用
echo sodium_crypto_aead_aes256gcm_is_available(), PHP_EOL; // 1

// 加密
$pem = sodium_crypto_aead_aes256gcm_encrypt($data, $ad, $nonce, $kengen);
var_dump($pem);
// string(28) "��VRw!�����f��l�O�tV=\x�"

// 解密
$v = sodium_crypto_aead_aes256gcm_decrypt($pem, $ad, $nonce, $kengen);
var_dump($v);
// string(12) "测试加密"

代码中的注释已经详细说明了相关函数及参数。在微信支付中使用这个来解密时,ad、key、nonce 等都是由微信提供过来的, 而我们这里做为演示,都是自己生成的内容。

sodium_crypto_aead_aes256gcm_encrypt() 加密生成的内容也是二进制的内容,所以相对来说也是非常安全的一种加密形式。

信息签名

Sodium 扩展库同样也为我们带来了验证数据是否被篡改的功能,也就是对信息进行签名比对的能力。

// 信息签名
$key = sodium_crypto_auth_keygen(); // 生成随机签名密钥
$message = '测试认证签名';

// 生成签名
$signature = sodium_crypto_auth($message, $key);
var_dump($signature);
// string(32) "�B�
//                9���l�wn�x���ӛc�ܙ�u^j��"

// 验证签名
var_dump(sodium_crypto_auth_verify($signature, $message, $key));
// bool(true)

它们需要的就是一个简单的随机签名密钥,然后通过对签名摘要和原文进行比对来确定数据在传输过程中是否被篡改。

Hash

是的,你没看错,Sodium 扩展也为我们提供了一套 Hash 加密的函数。不过它的使用要复杂一些, 生成的内容有点像 密码散列算法 生成的内容。 不过我们还是更推荐使用 密码散列算法 中的 password_hash() 来生成这类的 Hash 密码。

// Hash
$password = '测试Hash';
$hash = sodium_crypto_pwhash_str(
    $password,
    SODIUM_CRYPTO_PWHASH_OPSLIMIT_INTERACTIVE, // 最大计算量
    SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE // 最大RAM量
);
var_dump($hash);
// string(97) "$argon2id$v=19$m=65536,t=2,p=1$VFfdNV4W0MFwLiLPdr9i6g$QDmd5sQToZANYTVEkPVTbPvbY7tuf1ALKU3IXrF44R0"

// 验证 Hash 信息
var_dump(sodium_crypto_pwhash_str_verify($hash, $password));
// bool(true)

总结

虽说我们平常可能没接触过,但是确实在开发中 Sodium 扩展还是有实际应用的,既然微信都使用这种加密方式进行了数据加密, 我们也应该对它有更深入的了解。不过,还是希望官方能够尽早完善文档,否则也无法系统地学习这套扩展里面的内容。






参考资料

http://php.net/manual/zh/function.hash-hmac.php

http://www.pengray.com/Archive-view-id-3.html

https://blog.csdn.net/dengjiexian123/article/details/53313913

PHP 运算符:http://www.runoob.com/php/php-operators.html

PHP 位运算符:http://www.php.net/manual/zh/language.operators.bitwise.php

PHP 手册 函数参考 加密扩展 https://www.php.net/manual/zh/refs.crypto.php

PHP 手册 函数参考 加密扩展 Sodium https://www.php.net/manual/zh/book.sodium.php

PHP的Sodium加密扩展函数了解 https://blog.csdn.net/zhangyue0503/article/details/110607695

项目使用 libsodium 扩展 https://blog.csdn.net/u010324331/article/details/82153067

Using Libsodium in PHP Projects https://paragonie.com/book/pecl-libsodium/read/00-intro.md#installing-libsodium

PHP 手册 函数参考 加密扩展 OpenSSL https://www.php.net/manual/zh/book.openssl.php

PHP使用OpenSSL扩展 https://www.jianshu.com/p/567747d9558a


返回