加密算法兼容性:不同数据库、驱动版本对 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.7128 / 256ECB, CBC默认 ECB,不推荐直接使用
MySQL 8.0128 / 256ECB, CBC, GCMGCM 要求 MySQL 8.0.19+
PostgreSQL128 / 192 / 256 (pgcrypto)ECB, CBC, CFB, OFB, CTR需安装 pgcrypto 扩展
Oracle128 / 256CBC, GCM (DBMS_CRYPTO)DBMS_CRYPTO 默认 CBC
SQL Server128 / 192 / 256CBC, 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") 才解决。

我的建议:

  1. 写 SQL 或调用加密函数时,显式指定 模式和填充:AES_ENCRYPT(data, key, 'cbc'),不要依赖数据库默认值。
  2. 如果跨数据库,统一的方案是:只在应用层做加解密,数据库只存 BLOB 或 VARBINARY,不碰数据库内置加密函数。
加密算法兼容性决策流程 选择加密算法 检查目标数据库原生支持 原生支持 → 检查模式/填充 需要插件 → 验证版本兼容 检查驱动版本及加密接口 测试全链路(含边界数据) 确认方案并固化参数 最后一步:在文档中写明数据库版本、驱动版本、加密模式、IV 长度、填充方式,供后续维护参考
省心做法: 构建一个加密抽象层,屏蔽数据库差异。比如用 Java 的 Cipher 统一加解密,数据库只存 VARBINARY。这样换数据库时只需要改连接串,加密逻辑完全不动。我在公司内部推了这套方案,后续三次数据库迁移都顺了。

加密算法的兼容性问题,说到底是各个实现方对标准理解有偏差。你没法改变数据库厂商,但你能主动约束自己的参数选择——永远显式指定模式、IV 和填充,永远在升级前跑回归测试。这块偷懒,后面就得加班。