加密算法兼容性:不同数据库、驱动版本对 AES/SM4 的支持问题
加密算法选完了,代码写好了,一上预发环境就报错。这种情况我碰上不下十回。说白了,AES 和 SM4 在不同数据库、不同驱动版本里的实现细节,远比你想象的要碎。这一章我把这些年踩过的坑摊开来说清楚。
数据库对 AES 算法的支持差异
每个数据库对 AES 的支持深度不一样。有的只认 128 位密钥,有的对 GCM 模式支持得磕磕绊绊。我自己最深的印象是 MySQL 5.7 升 8.0 时,AES_ENCRYPT 函数默认行为变了——5.7 用 ECB+PKCS7,8.0 改成了 CBC+PKCS7。结果一堆老程序解密乱码。
| 数据库 | AES 密钥长度 | 支持模式 | 注意事项 |
|---|---|---|---|
| MySQL 5.7 | 128 / 256 | ECB, CBC | 默认 ECB,不推荐直接使用 |
| MySQL 8.0 | 128 / 256 | ECB, CBC, GCM | GCM 要求 MySQL 8.0.19+ |
| PostgreSQL | 128 / 192 / 256 (pgcrypto) | ECB, CBC, CFB, OFB, CTR | 需安装 pgcrypto 扩展 |
| Oracle | 128 / 256 | CBC, GCM (DBMS_CRYPTO) | DBMS_CRYPTO 默认 CBC |
| SQL Server | 128 / 192 / 256 | CBC, GCM | 通过列级加密或 Always Encrypted |
注意: 别指望数据库内置函数帮你搞定一切。跨数据库迁移时,
AES_DECRYPT 和 DECRYPT 的参数顺序、返回值类型都可能不一样。我在一个异构同步项目里吃过这个亏——MySQL 加密的数据在 PostgreSQL 里解不出来,最后只能把所有加解密挪到应用层统一处理。
驱动版本的加密函数实现差异
数据库层面过了,驱动层又给你挖坑。以最常用的 JDBC 驱动为例:
- MySQL Connector/J 5.x:不支持
getBytes("UTF-8")之外的字符集转换,GCM 模式基本废了。 - MySQL Connector/J 8.x:开始支持
!useSSL之外的加密扩展,但 AES-256-GCM 需要手动指定connectionCollation。 - PostgreSQL JDBC 42.x:对
pgcrypto接口封装有变化,42.2.x 与 42.5.x 处理gen_salt的方式不同。
看一段 Java 里调用 MySQL 加密的对比代码:
// Connector/J 5.x 写法(容易踩坑)
String sql = "SELECT AES_ENCRYPT(?, 'mykey')";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "Hello"); // 隐含使用数据库字符集,可能导致不同编码
// Connector/J 8.x 推荐写法
String sql = "SELECT AES_ENCRYPT(CAST(? AS CHAR CHARACTER SET utf8mb4), 'mykey')";
PreparedStatement ps = conn.prepareStatement(sql);
ps.setString(1, "Hello");
我的建议: 驱动版本升级后,一定要在测试环境跑一遍加密全链路。别只看功能测试,把数据长度、特殊字符、边界条件都覆盖到。我曾经因为从 Connector/J 5.1.49 升级到 8.0.28,线上几千条加密记录解不开了——就因为驱动默认编码从 latin1 变成了 utf8mb4,AES 加密结果不一致。
SM4 国密算法的数据库兼容现状
SM4 的兼容性问题比 AES 更头疼。原生支持的数据库少,多用插件或扩展。
| 数据库 | SM4 支持方式 | 已知问题 |
|---|---|---|
| 达梦 DM8 | 内置 SM4 函数(SM4_ENCRYPT) | 密钥长度固定 128 位,无填充选项 |
| OceanBase | 通过 PL 扩展实现 | 不同 OB 版本接口签名不一致 |
| MySQL / PostgreSQL | 需安装 third-party 插件 | 插件作者不同,参数顺序各异 |
| 人大金仓 KingbaseES | 内置 SM4 模式(CBC/ECB) | V8 版本默认 ECB,V9 改为 CBC |
我印象很深的一个项目: 把业务从 MySQL 迁移到达梦,达梦的 SM4_ENCRYPT 函数要求密钥必须是十六进制字符串,而 MySQL 那边用的二进制密钥。改应用层适配逻辑改了一周,每天都要和 DBA 对参数。
核心结论: SM4 的兼容性瓶颈不在算法本身,而在数据库厂商对国标 GM/T 0002 的解读不一致。同样的明文、同样的密钥,在不同数据库里加密结果可能完全不同。务必用同一份标准测试向量(如 GM/T 0002 附录 A 的测试数据)做交叉验证。
加密模式与填充方式的兼容性陷阱
这个问题看起来基础,但我敢说至少一半的线上加密故障都栽在这里。
- 默认模式差异: MySQL 5.7 的
AES_ENCRYPT默认用 ECB,Oracle 的DBMS_CRYPTO默认用 CBC,PostgreSQL 的pgcrypto默认用 CTR。 - 填充方式黑洞: 多数数据库内置函数默认走 PKCS7 填充。但如果你在应用层自己实现了
Cipher.getInstance("AES/CBC/NoPadding"),而数据库那边却用了 PKCS7,长度不对时解密直接失败。 - GCM 模式的 IV 问题: 不同驱动对 GCM 的 nonce(IV)长度要求不一样。MySQL 8.0 要求 12 字节,但某些第三方 JDBC 驱动默认为 16 字节。解密时 IV 不匹配,抛出
AEADBadTagException。
我曾经因此栽过: 一个金融项目,AES-256-GCM 加密,MySQL 8.0.20 配合 Connector/J 8.0.23。一切正常。后来数据库升级到 8.0.28,驱动没升,结果所有新写入的数据都带上了一个固定 IV 前缀,旧数据解密时前缀不匹配直接崩。排查了两天才发现是驱动版本识别了数据库新特性,自动改了 IV 构造逻辑。最后加了一行
connectionProperties.setProperty("encryptionMode","AES256GCM_OLD") 才解决。
我的建议:
- 写 SQL 或调用加密函数时,显式指定 模式和填充:
AES_ENCRYPT(data, key, 'cbc'),不要依赖数据库默认值。 - 如果跨数据库,统一的方案是:只在应用层做加解密,数据库只存
BLOB或VARBINARY,不碰数据库内置加密函数。
省心做法: 构建一个加密抽象层,屏蔽数据库差异。比如用 Java 的
Cipher 统一加解密,数据库只存 VARBINARY。这样换数据库时只需要改连接串,加密逻辑完全不动。我在公司内部推了这套方案,后续三次数据库迁移都顺了。
加密算法的兼容性问题,说到底是各个实现方对标准理解有偏差。你没法改变数据库厂商,但你能主动约束自己的参数选择——永远显式指定模式、IV 和填充,永远在升级前跑回归测试。这块偷懒,后面就得加班。