学习PHP扩展库OpenSSL的对称加密相关知识
进入PHP7之后Mcrypt扩展逐步被弃用,学习替代者OpenSSL扩展就显得很有必要了
学习该扩展之前,先温习一下和加解密相关的关键词:
对称加密
加密与解密使用同一个密码。
非对称加密
加密与解密使用不同的密码(一个公钥,一个私钥)。
公钥/私钥
字面意思: 公钥表示能公开的密钥,私钥表示不能公开的私密的密钥。
公钥与密钥是通过算法生成的一个密钥对,具有唯一性并且可以互相解密: 使用公钥加密的数据能用私钥解密,使用私钥加密的数据能用公钥解密。
签名
使用私钥对数据加密,这个过程就是签名。
验证签名
对使用私钥加密的数据通过公钥解密,这个过程就是验证签名。
介绍下对称加密所用到的几个相关函数:
// 获取到OpenSSL扩展所支持的所有加密算法
openssl_get_cipher_methods()
// 获取指定加密算法的初始化向量(iv)所需要长度(长度单位Byte)
// 因每种算法在加解密时需要的iv值长度不同
openssl_cipher_iv_length($method)
// 随机生成指定长度的随机字节串(不是字符串)
// 字节串直接打印是乱码哦
openssl_random_pseudo_bytes($length)
// 作用同上,不同点是该函数属于PHP7内置
random_bytes($length)
// 加密与解密,除参一外,其他参数完全相同
// 加密时$data是需要加密的数据,解密时$data是需要解密的数据
openssl_encrypt($data, $method, $key, $options = 0, $iv = '')
openssl_decrypt($data, $method, $key, $options = 0, $iv = '')
加密函数与解密函数需要详细的记录一下:
- data: 明文(对应需要加密和解密的数据)
- method: 加密的算法(openssl_get_cipher_methods已列出所有算法)
- key: 密钥(不同的算法有效长度不同)
- options: 作用是修饰明文,控制函数返回数据格式。值有0、OPENSSL_RAW_DATA对应1、OPENSSL_ZERO_PADDING对应2
- 0: 自动对明文填充,返回数据经base64编码处理
- 1: 自动对明文填充,返回原始字节串数据
- 2: 官方不推荐,忽略即可。
- iv: 初始化向量,不同算法对此长度要求不同。不使用该参数会抛出警告
以上参数中关于密钥的长度、初始化向量的长度问题需要再次细说下:
主流的对称加密方式有DES、AES(算法名称以此开头),这两种加密方式都属于密码分组加密。
DES加密方式的密钥长度为64bit(默认UTF-8编码),也就是8个字节(1字节=8bit),密钥不足8字节将使用0补充。加密过程大体是将明文按照64bit长度进行分组,最后一组不足64位时需要填充数据。分组后明文组与密钥按位替代或交换的方法形成密文组。
AES加密方式的分组长度是128bit。密钥的长度根据不同的加密算法不同,有128bit(AES-128=16字节)、192bit(AES-192)、256bit(AES-256)。与DES方式相同,密钥长度不足时同样使用0补充,超过该长度的部分无效。(目的取代DES)
在很多加密算法名字中带有“CBC”字样(算法名称以此结尾)。它们都使用同一种加密模式:密码分组链接模式(Cipher Block Chaining)。
使用CBC模式加密时,明文会被分成若干个组,以组为单位加密。每个组的加密过程,依赖他前一个组的数据,因为要跟前一组的数据进行异或操作后生成本组的密文。那么最开头的那个组又要依赖谁呢?依赖的就是IV,这就是名称叫初始化向量(initialization vector)的原因。
所以CBC加密模式必须使用指定长度的IV,而像ECB模式则不需要IV。关于加密方式与加密模式等详细准确的内容,则需要感兴趣的道友自己查阅。
结合以上内容大致能明白各种算法在加密与解密时key与iv的取值了。
// 加密数据
$data = '我的要加密数据';
// 加密算法
$method = 'AES-128-CBC';
// KEY的长度不能通过任何函数得到,AES-128-CBC有效密钥长度为16位
$key = random_bytes(16);
// 生成IV
$len = openssl_cipher_iv_length($method);
$iv = $len ? random_bytes($len) : '';
// 加密
$encrypted = openssl_encrypt($data, $method, $key, 0, $iv);
// 解密
$decrypted = openssl_decrypt($encrypted, $method, $key, 0, $iv);
// 不固定FzqBms+WFJREL4sSVEWJh5IQfYyTf709u/jundwn2as=
var_dump($encrypted);
// 我的要加密数据
var_dump($decrypted);
以上示例有个严重缺陷:数据在加密和解密时,key与iv必须使用同一个。所以单纯的作为栗子可以,实际在加密后,加密数据、key、iv三者都要存储起来,在下次解密数据时,必须使用加密时的key与iv才能正确解密数据。
通过查看Laravel加密相关源码,我发现它将iv巧妙地加入到加密数据中,解密时先从加密字符中拿到iv,再进行正式解密。这样只需要将加密数据与key存储,解密也只需要加密数据和key即可完成解密工作。
/**
* 加密解密工具类
* 密钥通过generateKey静态方法生成
* 如需存储密钥则需要先将其bin2hex转成十六进制
* 存储的密钥在实例化该类前将其hex2bin转成字节
*/
class Encrypter
{
// 加密密钥
protected $key;
// 用于加密的算法
protected $cipher;
/**
* 创建加密器实例
* @param string $key 密钥-根据算法不同决定位数[AES-128-CBC=16位,AES-256-CBC=32位]
* @param string $cipher 算法-[AES-128-CBC | AES-256-CBC]
* @throws Exception
*/
public function __construct($key, $cipher = 'AES-128-CBC')
{
$key = (string)$key;
if (static::supported($key, $cipher)) {
$this->key = $key;
$this->cipher = $cipher;
} else {
throw new Exception('密钥错误,算法AES-128-CBC与AES-256-CBC分别需要16位或32位密钥');
}
}
/**
* 根据指定算法生成随机密钥
* @param string $cipher
* @return string
* @throws Exception
*/
public static function generateKey($cipher = 'AES-128-CBC')
{
return random_bytes($cipher === 'AES-128-CBC' ? 16 : 32);
}
/**
* 加密给定字符串
* @param $string
* @return string
* @throws Exception
*/
public function encrypt($string)
{
$iv = random_bytes(openssl_cipher_iv_length($this->cipher));
$value = \openssl_encrypt($string, $this->cipher, $this->key, 0, $iv);
if ($value === false) {
throw new Exception('数据加密失败[ENCRYPT]');
}
$mac = $this->hash($iv = base64_encode($iv), $value);
$json = json_encode(compact('iv', 'value', 'mac'), JSON_UNESCAPED_SLASHES);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception('数据加密失败[HASH]');
}
return base64_encode($json);
}
/**
* 解密给定字符串
* @param $payload
* @return string
* @throws Exception
*/
public function decrypt($payload)
{
$payload = $this->getJsonPayload($payload);
$iv = base64_decode($payload['iv']);
$decrypted = \openssl_decrypt($payload['value'], $this->cipher, $this->key, 0, $iv);
if ($decrypted === false) {
throw new Exception('数据解密失败[DECRYPT]');
}
return $decrypted;
}
/**
* 验证密钥是否与给定的算法相匹配
* @param string $key 密钥
* @param string $cipher 算法
* @return bool
*/
protected static function supported($key, $cipher)
{
$length = mb_strlen($key, '8bit');
return ($cipher === 'AES-128-CBC' && $length === 16) ||
($cipher === 'AES-256-CBC' && $length === 32);
}
/**
* HASH加密
* @param string $iv
* @param mixed $value
* @return string
*/
protected function hash($iv, $value)
{
return hash_hmac('sha256', $iv . $value, $this->key);
}
/**
* 解密JSON数据
* @param $payload
* @return mixed
* @throws Exception
*/
protected function getJsonPayload($payload)
{
$payload = json_decode(base64_decode($payload), true);
if (!$this->validPayload($payload)) {
throw new Exception('The payload is invalid.');
}
if (!$this->validMac($payload)) {
throw new Exception('The MAC is invalid.');
}
return $payload;
}
/**
* 验证加密数据是否有效
* @param mixed $payload
* @return bool
*/
protected function validPayload($payload)
{
return is_array($payload) && isset($payload['iv'], $payload['value'], $payload['mac']) &&
strlen(base64_decode($payload['iv'], true)) === openssl_cipher_iv_length($this->cipher);
}
/**
* 验证HASH加密数据是否有效
* @param array $payload
* @return bool
* @throws Exception
*/
protected function validMac(array $payload)
{
$calculated = $this->calculateMac($payload, $bytes = random_bytes(16));
return hash_equals(hash_hmac('sha256', $payload['mac'], $bytes, true), $calculated);
}
/**
* 计算给定负载的散列
* @param array $payload
* @param string $bytes
* @return string
*/
protected function calculateMac($payload, $bytes)
{
return hash_hmac('sha256', $this->hash($payload['iv'], $payload['value']), $bytes, true);
}
}
记得要分清bit与byte的关系哦~