1. 云栖社区>
  2. PHP教程>
  3. 正文

MySQL 使用 AES 加解密資料、PHP 產生相容 MySQL 的加解密效果

作者:用户 来源:互联网 时间:2017-11-30 20:46:24

mysqlphp解密aes使用效果相容

MySQL 使用 AES 加解密資料、PHP 產生相容 MySQL 的加解密效果 - 摘要: 本文讲的是MySQL 使用 AES 加解密資料、PHP 產生相容 MySQL 的加解密效果, MySQL 使用 AES_ENCRYPT()、AES_DECRYPT() 两个函式可将资料进行 AES 的加解、解密。 所以若资料库中有某些栏位是隐私资料想加密后再储存到资料表中。只要 INSERT 到资料表时用 AES_ENCRYPT(

MySQL 使用 AES_ENCRYPT()、AES_DECRYPT() 两个函式可将资料进行 AES 的加解、解密。


所以若资料库中有某些栏位是隐私资料想加密后再储存到资料表中。只要 INSERT 到资料表时用 AES_ENCRYPT() 将资料先加密SELECT 时用 AES_DECRYPT() 将资料解密还原即可。


AES_ENCRYPT() 加密语法为 AES_ENCRYPT(str,key_str[,init_vector])

str原始字串

key_str自行设定的密钥

init_vector此参数 MySQL 5.6.17 之后才有。且多了block_encryption_mode
环境变数可设定不同演算法。测试环境为 MariaDB 5.5演算法是 ECB没用到此参数后面都以 AES-128-ECB 演算法加解密测试。
AES_DECRYPT() 解密语法为 AES_DECRYPT(crypt_str,key_str[,init_vector])

crypt_str加密后的二进位资料

key_str自行设定的密钥

init_vectorECB 演算法没用到此参数

解密失败会回传 null也可能回传非 null 的垃圾资料。

「If AES_DECRYPT() detects invalid data or incorrect padding, it returns NULL. However, it is possible for AES_DECRYPT() to return a non-NULL value (possibly garbage) if the input data or the key is invalid.」
密钥长度AES-128-ECB 密钥长度为 128bits (As of MySQL 5.6.17, key lengths of 196 or 256 bits can be used)但使用 AES_ENCRYPT()、AES_DECRYPT() 时如果输入太短或太长的密钥MySQL 会自动处理成 128bits
原始字串 str、加密后资料 crypt_str 可以是任意长度。

而 AES-128-ECB 演算法(128bits)原资料须为 16bytes 的倍数所以原始字串 str 加密前AES_ENCRYPT() 会自动将长度填充为AES加密演算法须要的区块倍数长度(16byte的倍数)解密时 AES_DECRYPT() 再将填充的字元移除。

至于用来填充的字元则是用原始字串 str 须要再补多少长度才会是的16byte倍数的char值(所补长度数字取char得到的字元、ASCII对应的字元)且若原字串长度刚好为 16bytes 倍数时也会再填充一个完整的 16bytes 区块。如此反解后只须由最后一个字元即可知反解后的字串最后面多少长度是填充的才能将填充后的字串去除得到原始字串 str。
将加密完的密文储存到资料表所以须知道 AES_ENCRYPT() 回传的资料型态、资料长度。

资料型态加密后的资料为二进位资料。(若 str、key_str 有任一个为 nullAES_ENCRYPT 将回传 null)

资料长度资料加密后的长度计算方式「16 * (trunc(string_length / 16) + 1)
」其中 trunc() 是虚拟程式码(pseudo code)表示小数部分无条件捨去。
测试加密前后资料长度
将"ABC"资料用"testkey"当作密钥加密前后的长度变化SELECT LENGTH("ABC"); //3 bytes
SELECT LENGTH(AES_ENCRYPT("ABC","testkey")); //16 bytes
SELECT LENGTH(AES_ENCRYPT("ABC","testkey123456790")); //16 bytes
加密后的长度为 16*(trunc(3/16)+1)=16 bytes
将"1234567890ABCDEF"资料用"testkey"当作密钥加密前后的长度变化SELECT LENGTH("1234567890ABCDEF"); //16
SELECT LENGTH(AES_ENCRYPT("1234567890ABCDEF","testkey")); //32 bytes
加密后的长度为 16*(trunc(16/16)+1)=32 bytes
将"1234567890ABCDEFG"资料用"testkey"当作密钥加密前后的长度变化SELECT LENGTH("1234567890ABCDEFG"); //17
SELECT LENGTH(AES_ENCRYPT("1234567890ABCDEFG","testkey")); //32 bytes
加密后的长度为 16*(trunc(17/16)+1)=32 bytes

将"一二三四五六七八九十一二三四五六"资料用"testkey"当作密钥加密前后的长度变化


SELECT LENGTH("一二三四五六七八九十一二三四五六"); //48
SELECT LENGTH(AES_ENCRYPT("一二三四五六七八九十一二三四五六","testkey")); //64
SELECT c, LENGTH(c ), CHAR_LENGTH(c), LENGTH(AES_ENCRYPT(c, "testkey")) FROM zz;
mysql> SELECT c, LENGTH(c), CHAR_LENGTH(c), LENGTH(AES_ENCRYPT(c, "testkey")) FROM test;

+-------------------------------+------------+----------------+-----------------------------------+ | c | LENGTH(c ) | CHAR_LENGTH(c) | LENGTH(AES_ENCRYPT(c, "testkey")) | +-------------------------------+------------+----------------+-----------------------------------+ | 一二三四五六七八九十一二三四五六 | 48 | 16 | 64 | +-------------------------------+------------+----------------+-----------------------------------+


可发现UTF8中常用的中文一个字是3 bytes所以16个中文字是16*3=48 bytes

加密后的长度为 16*(trunc((16*3)/16)+1)=64 bytes

所以假设原本 varchar(16)要改用 varbinary 储存加密后的结果至少须设为 varbinary(64) 若只设 varbinary(63) 或 varbinary(16)、varbinary(32)长度都不够加密资料无法全部储存将无法正确反解。



加解密写入资料表测试

(PHP、MySQL用的 PHPMyAdmin 版本常用手动改回不用16进位显示2进位所以直接用PHP测试)

//$db PDO物件
$stmt = $db->query("CREATE TABLE IF NOT EXISTS `zz` (`id` int(11) NOT NULL, `c` varchar(255) NOT NULL, `c_aes` varbinary(2) DEFAULT '', PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8");
$stmt = $db->query("ALTER TABLE zz CHANGE c_aes c_aes VARBINARY(64) NULL DEFAULT ''"); //可修改不同 VARBINARY 长度测试
$stmt = $db->query("INSERT INTO zz (id, c, c_aes) VALUES (1, '一二三四五六七八九十一二三四五六', '') ON DUPLICATE KEY UPDATE c=VALUES(c)"); //可修改不同加密内容'一二三四五六七八九十一二三四五六'测试
$stmt = $db->query("UPDATE zz SET c_aes = AES_ENCRYPT( c, 'testkey')");
$stmt = $db->query("SELECT c, c_aes, LENGTH(c), CHAR_LENGTH(c), LENGTH(AES_ENCRYPT(c, 'testkey')) , AES_DECRYPT(AES_ENCRYPT(c,'testkey'),'testkey'), LENGTH(c_aes), AES_DECRYPT(c_aes,'testkey'), LENGTH(AES_DECRYPT(c_aes, 'testkey')) FROM zz");
var_dump($stmt->fetch(/PDO::FETCH_ASSOC));
array(9) {
["c"]=> //加密前字串
string(48) "一二三四五六七八九十一二三四五六"
["c_aes"]=> //加密后二进位资料
string(64) "? Lmy; M i 3[email protected]rp'ƒ ا x /r%O *e:y"
["LENGTH(c)"]=> //加密前byte长度
string(2) "48"
["CHAR_LENGTH(c)"]=> //加密前字数
string(2) "16"
["LENGTH(AES_ENCRYPT(c, 'testkey'))"]=> //加密后byte长度
string(2) "64"
["AES_DECRYPT(AES_ENCRYPT(c,'testkey'),'testkey')"]=> //解密后字串
string(48) "一二三四五六七八九十一二三四五六"
["LENGTH(c_aes)"]=> //加密后储存到资料表的的二进位资料byte长度(VARBINARY太短时可观察到被截断)
string(2) "64"
["AES_DECRYPT(c_aes,'testkey')"]=> //将资料表储存的加密二进位资料解密后的字串(若储存后已被截断会无法正常解密)
string(48) "一二三四五六七八九十一二三四五六"
["LENGTH(AES_DECRYPT(c_aes, 'testkey'))"]=> //资料表储存的加密二进位资料解密后的字串byte长度
string(2) "48"
}
PHP 可以用openssl_*、mcrypt_* 两种方法进行 AES 加解密但 mcrypt PHP 7.1 之后已不建议使用。以下是分别使用 openssl、mcrypt 产生跟 MySQL 相同的加解密结果。
openssl主要须处理密钥 key 超过 16 bytes 的部分MySQL 会对过长部分进行 XOR 运算PHP 测试结果似乎是将过长部分截断。/**
* 模拟 MySQL AES_ENCRYPT()、AES_ENCRYPT()
*/
class AesMySQL {
/**
* 原始的密钥字串
* @var string
*/
private $key_str;
/**
* 处理后符合规则的密钥字串
* @var string
*/
private $key;
/**
* 将原始的密钥字串处理成 MySQL AES_ENCRYPT() 使用的密钥格式
* @param string $key_str 原始的密钥字串
* @return string
*/
public function getAesKey($key_str) {
if (isset($this->key_str) && $key_str === $this->key_str) {
//此原始的密钥字串已处理过
} else {
//PHP测试超过16bytes的部分似乎会截断。
//MySQL超过16bytes依序每16bytes分成一组每一组同位置的位元组进行XOR运算最终处理成只有16bytes
//若原始长度小于16bytesPHP、MySQL都是在后面用 chr(0) 补齐chr(0)即"/0"
$key_len = 16; //处理成16bytes
$key_str_len = strlen($key_str);
if ($key_str_len <= $key_len) {
$pad = $key_len - $key_str_len;
$key = $key_str . str_repeat("/0", $pad); //"/0" 可用 chr(0) 替代
} else {
$key = substr($key_str, 0, $key_len);
for ($i = $key_len; $i < $key_str_len; $i++) {
$pos = $i % $key_len;
$key[$pos] = $key[$pos] ^ $key_str[$i];
}
}
$this->key_str = $key_str;
$this->key = $key;
}
return $this->key;
}
/**
* 模拟 MySQL AES_ENCRYPT() 加密结果 (使用openssl_encrypt)
* @param string $str
* @param string $key_str 原始的密钥字串
* @return binary
*/
public function aesEncrypt($str, $key_str) {
//openssl_get_cipher_methods()可取得可用的演算法列表
$cipher = "AES-128-ECB"; //MySQL使用 128bit ECB 演算法
$key = $this->getAESKey($key_str); //密钥用MySQL的规则再处理过(测试原本PHP太长超过16bytes的部分会截断)
$options = OPENSSL_RAW_DATA; //OPENSSL_RAW_DATA、OPENSSL_ZERO_PADDING
//OPENSSL_RAW_DATA 会自动使用 PKCS#7 格式填充所以加解密不须自己处理填充问题
//OPENSSL_ZERO_PADDING 须自己处理填充(加密前自行加上填充、解密后自行去除填充)且回传格式为 Base64
///2014th7cj/d/file/p/20171027/function.openssl-encrypt.php = openssl_encrypt($str, $cipher, $key, $options); //ECB没使用iv
return $ciphertext_raw;
}
/**
* 模拟 MySQL AES_DECRYPT() 解密结果 (使用openssl_encrypt)
* @param binary $crypt_str
* @param string $key_str 原始的密钥字串
* @return string
*/
public function aesDecrypt($crypt_str, $key_str) {
$cipher = "AES-128-ECB"; //MySQL使用 128bit ECB 演算法
$options = OPENSSL_RAW_DATA;
$key = $this->getAESKey($key_str);
$original_plaintext = openssl_decrypt($crypt_str, $cipher, $key, $options);
return $original_plaintext;
}
}

mcrypt须处理密钥长度过短、过长以及加密内容的填充/**
* 模拟 MySQL AES_ENCRYPT()、AES_ENCRYPT()
*/
class AesMySQL_Old {
/**
* 原始的密钥字串
* @var string
*/
private $key_str;
/**
* 处理后符合规则的密钥字串
* @var string
*/
private $key;
/**
* 将原始的密钥字串处理成 MySQL AES_ENCRYPT() 使用的密钥格式
* @param string $key_str 原始的密钥字串
* @return string
*/
public function getAesKey($key_str) {
if (isset($this->key_str) && $key_str === $this->key_str) {
//此原始的密钥字串已处理过
} else {
//PHP只接受刚好 16、24、32 bytes 长度的字串。
//MySQL接受任何长度的字串
// 长度小于16bytesMySQL在后面用 chr(0) 补齐
// 若超过16bytes依序每16bytes分成一组每一组同位置的位元组进行XOR运算处理成只有16bytes
$key_len = 16; //处理成16bytes
$key_str_len = strlen($key_str);
if ($key_str_len <= $key_len) {
$pad = $key_len - $key_str_len;
$key = $key_str . str_repeat("/0", $pad); //"/0" 可用 chr(0) 替代
} else {
$key = substr($key_str, 0, $key_len);
for ($i = $key_len; $i < $key_str_len; $i++) {
$pos = $i % $key_len;
$key[$pos] = $key[$pos] ^ $key_str[$i];
}
}
$this->key_str = $key_str;
$this->key = $key;
}
return $this->key;
}
/**
* 模拟 MySQL AES_ENCRYPT() 加密结果 (使用mcrypt_encryptPHP7.1以上已不建议使用)
* @param string $str
* @param string $key_str 原始的密钥字串
* @return binary
*/
public function aesEncrypt($str, $key_str) {
$cipher = MCRYPT_RIJNDAEL_128;
$key = $this->getAESKey($key_str);
//使用 mcrypt_encrypt 须自行先将填充做好避免预设自行填充"/0"
//(If the size of the data is not n * blocksize, the data will be padded with '/0'.)
$blocksize = 16; //须为16bytes的倍数
$text = $this->pkcs5Pad($str, $blocksize); //使用PKCS#5填充
$mode = MCRYPT_MODE_ECB;
$encrypted_val = mcrypt_encrypt($cipher, $key, $text, $mode); //ECB没使用iv
return $encrypted_val;
}
/**
* 模拟 MySQL AES_DECRYPT() 解密结果 (使用mcrypt_encryptPHP7.1以上已不建议使用)
* @param binary $crypt_str
* @param string $key_str 原始的密钥字串
* @return string
*/
public function aesDecrypt($crypt_str, $key_str) {
$cipher = MCRYPT_RIJNDAEL_128;
$key = $this->getAESKey($key_str);
$mode = MCRYPT_MODE_ECB;
$original_plaintext = mcrypt_decrypt($cipher, $key, $crypt_str, $mode);
$original_plaintext = $this->pkcs5Unpad($original_plaintext); //去除PKCS#5填充的字元
return $original_plaintext;
}
/**
* 填充不足字节数(PKCS#5)
* 1.将填充长度取chr()当填充值
* 2.刚好满$blocksize倍数则再填充一组$blocksize大小
* @param string $text
* @param int $blocksize
* @return string
*/
private function pkcs5Pad($text, $blocksize) {
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}
/**
* 去除 pkcs5Pad() 的填充值(PKCS#5)
* @param string $text
* @return string|false
*/
private function pkcs5Unpad($text) {
$pad = ord($text{strlen($text) - 1});
if ($pad > strlen($text)) {
return false;
}
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
return false;
}
return substr($text, 0, -1 * $pad);
}
}

使用 PHP 加解密可减轻 MySQL 负担、不用在 MySQL Server 执行包含密钥的 SQL 指令当然其实加解密方式可以不用做成跟 MySQL 相容。

但两者相容的处理方式若有一天需要直接使用 SQL 指令的 WHERE 条件过滤加密前的资料时便可派上用场。

参考


以上是云栖社区小编为您精心准备的的内容,在云栖社区的博客、问答、公众号、人物、课程等栏目也有的相关内容,欢迎继续使用右上角搜索按钮进行搜索mysql , php , 解密 , aes , 使用 , 效果 相容 ,以便于您获取更多的相关知识。

弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率

40+云计算产品,6个月免费体验

现在注册,免费体验40+云产品,及域名优惠!

云服务器9.9元/月,大学必备