最近需要给数据库存储的敏感数据进行加密,手机号身份证号之类的,本想只要全部hash一遍就可以了,筛选的时候只需要把接收到的数据一样hash就能匹配上了。

但是考虑到可能有需要到模糊匹配的需求,以及某些时候需要返回给前台原始数据的信息,所以直接使用hash单向加密似乎过于暴力了,毕竟加密也只是需要数据库信息单独展示的时候无法被分辨,所以这个需求下也许对称加密是最好的方式。

之前写过一篇关于AES的文章,大概的认识了下这个算法,所以这次就使用AES的方式加密。

后台是PHP,数据库是MySQL。


由于是基于现有的系统改造,所以我们需要修改大量的旧数据,这里用较为方便的原生的MySQL语句来批量执行加密操作,这也是我们后面构建后端加密逻辑的基础:

-- 对user表中的phone字段进行加密,处理表中的全部数据
UPDATE `user` SET phone=HEX(AES_ENCRYPT(phone, SUBSTR(SHA2("encrypt_key",256),1,16)));

-- 筛选解密获取原始数据
select AES_DECRYPT(UNHEX(phone),SUBSTR(SHA2(("encrypt_key",256),1,16)) FROM `user`;

加密的SQL语句比较简单,有两个点需要注意:

  1. AES_ENCRYPT()加密之后需要用HEX()转为十六进制字符串,否则原始的二进制数据输出无法存储在varchar格式的字段中
  2. 在使用口令加密前需要切出前16位。这是因为在PHP中openssl_encrypt()函数接受的口令存在一个默认的长度限制,超出限制就会被静默截断,如果你使用的语言调用加密函数可以控制截断或没有截断则不需要SUBSTR操作


下面的代码展示了PHP中AES加密的基本流程:

<?
$key = "encrypt_key";
$str = "12312341234";
$cipherAlgo = "aes-128-ecb";
$hashKey = hash('sha256', $key);

// 加密
$encryptStr = openssl_encrypt($str , $cipherAlgo, $hashKey);
// byHeNZonWNZGtHQkmmeeNA==
var_dump($encryptStr);

// 存储到数据库中使用格式,与数据库自带的加密方式同步
$dbStr = strtoupper(bin2hex(base64_decode($encryptStr)))
// 6F21DE359A2758D646B474249A679E34
var_dump($dbStr);

// 解密
$decryptStr = openssl_decrypt(base64_encode(hex2bin($encryptStr)), $cipherAlgo, $hashKey);

$key是AES加密所需的口令,在实际使用的时候,为安全起见最好先对它进行一次hash再用来加密。

上面的代码中使用的格式是aes-128-ecb,主要是由于这是MySQL中原生的加密函数AES_DECRYPT()默认使用的加密格式。这里为了方便MySQL统一处理旧数据所以由后端向数据库端妥协,虽然这里用到的是MySQL中的默认加密格式,但实际上使用中并不建议在查询数据库时调用AES_DECRYPT()函数操作,这些复杂的加解密逻辑操作应该放到后端中处理。

直接使用openssl_encrypt()加密的结果是如byHeNZonWNZGtHQkmmeeNA==这样的数据。我们先对其base64_decode(),然后转换为十六进制字符串,再转为大写。得到的数据是6F21DE359A2758D646B474249A679E34这样的形式,这和直接在数据库中使用HEX(AES_ENCRYPT())的结果相同。

这样我们就完成了加密解密敏感数据的逻辑。


Ref

  1. https://www.php.net/manual/zh/function.openssl-encrypt
  2. https://stackoverflow.com/questions/49236781/php-openssl-decrypt-an-aes-mysql-encryption
  3. https://www.sjkjc.com/mysql-ref/aes_decrypt/
  4. https://dev.mysql.com/doc/mysql-secure-deployment-guide/5.7/en/secure-deployment-block-encryption-mode.html