4. 常见加密模式:AES-GCM、RSA-OAEP、国密SM4

加密模式选对了,项目成功一半。选错了,后面全是坑。

我见过不少团队,上线前才发现加密方式不满足合规要求,或者性能根本扛不住。今天就聊聊三种最常见的模式——AES-GCM、RSA-OAEP、国密SM4——它们各自的适用场景、使用姿势和常见陷阱。

4.1 AES-GCM:对称加密的首选

先问个问题:对称加密里,CBC和GCM,你选哪个?

我个人习惯直接用GCM。为什么?因为它自带认证能力。

CBC模式只能保证机密性,没法防篡改。攻击者改一个密文块,你解密后得到的就是乱码,而且你不知道数据被动过。GCM不同,它会在加密的同时生成一个认证标签(Auth Tag),解密时校验这个标签。说白了就是既加密又防篡改。

GCM的核心特点

  • 认证加密(AEAD):加密+完整性校验一次完成
  • 支持附加数据(AAD):只校验不解密的头部信息
  • 并行计算:硬件加速友好,性能优秀
  • IV/Nonce要求:不能重复使用

⚠ 重要:IV/Nonce不能重复

我曾经在压测环境里踩过这个坑。GCM模式下,如果两次加密用了相同的Key和IV/Nonce,攻击者可以恢复出你的密钥。这不是理论风险,是真实可被利用的。

建议:每次加密都生成一个新的12字节随机IV,并和密文一起存储。

来看看代码怎么写。我用Java的Cipher实现举个例子:

// AES-GCM 加密示例(Java)
public static byte[] encryptAesGcm(byte[] plaintext, byte[] key, byte[] aad) throws Exception {
    // 生成12字节随机IV
    byte[] iv = new byte[12];
    SecureRandom.getInstanceStrong().nextBytes(iv);
    
    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
    GCMParameterSpec spec = new GCMParameterSpec(128, iv); // 128位认证标签
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), spec);
    
    // 设置附加数据(可选)
    if (aad != null && aad.length > 0) {
        cipher.updateAAD(aad);
    }
    
    byte[] ciphertext = cipher.doFinal(plaintext);
    
    // 返回 IV + 密文,方便解密时提取
    return ByteBuffer.allocate(iv.length + ciphertext.length)
            .put(iv)
            .put(ciphertext)
            .array();
}

解密时先提取前12字节作为IV,剩下的就是密文(含认证标签)。注意:标签长度推荐128位,不要为了省几个字节改用64位。

AAD(附加数据)是个好东西。我建议把用户ID、版本号等元信息放进去,这样如果有人篡改这些字段,解密会直接失败。

4.2 RSA-OAEP:非对称加密的现代方案

RSA直接加密?千万别这么干。

教科书式的RSA(即RSAES-PKCS1-v1_5)有已知的padding oracle攻击风险。而RSA-OAEP(Optimal Asymmetric Encryption Padding)是经过验证的现代方案。你想想看,如果RSA本身就是个确定性算法,相同的明文加密出相同的密文,这多危险——攻击者可以搞字典攻击。加padding就是给它注入随机性。

特性 RSA-OAEP RSA-PKCS1-v1_5
安全性 CCA安全(选择密文攻击安全) 存在已知漏洞
随机性 每次加密结果不同 确定性加密
填充开销 较大(约42字节+摘要长度) 较小(约11字节)
推荐程度 强烈推荐 ❌ 不建议使用

💡 我建议:新项目一律用RSA-OAEP。如果是改造老系统,也尽快迁移。我知道切换加密方案很麻烦,但安全债迟早要还。

使用RSA-OAEP时,有几个参数要注意:

  • 摘要算法:推荐SHA-256,别用SHA-1了
  • MGF1:掩码生成函数,同样用SHA-256
  • 标签(Label):可以传一个可选字符串,比如应用标识,增加安全性
// RSA-OAEP 加密示例(Java)
public static byte[] encryptRsaOaep(byte[] plaintext, PublicKey publicKey) throws Exception {
    Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
    cipher.init(Cipher.ENCRYPT_MODE, publicKey);
    return cipher.doFinal(plaintext);
}

注意:RSA有明文长度限制。比如2048位密钥,OAEP填充后最多只能加密190字节(2048/8 - 2*32 - 2)。如果字段超过这个长度,需要用混合加密方案。

混合加密怎么搞?我通常的做法:用RSA加密一个随机的AES密钥,然后用这个AES密钥去加密实际数据。既解决长度问题,又利用对称加密的高性能。

4.3 国密SM4:合规场景下的对称加密

说句实话,SM4在算法层面上和AES没什么本质区别,都是分组密码、128位密钥。但因为合规要求,很多国内项目必须用它。

嗯,这里要注意:SM4本身只是一个分组加密算法,和AES一样,它也有各种工作模式:ECB、CBC、GCM、CTR等。很多SDK默认只暴露ECB模式,千万别直接用——ECB模式相同的明文会得到相同的密文,结构信息完全暴露。

SM4模式选择建议

  • 推荐模式:SM4-GCM(认证加密,和AES-GCM完全一样)
  • 次选模式:SM4-CBC + HMAC
  • 不推荐:SM4-ECB、SM4-CBC不搭配HMAC

不过要注意:不是所有国密SDK都实现了GCM模式。如果SDK不支持,你可以用SM4-CBC,但一定要加上HMAC做完整性校验。我在一个金融项目里就遇到过这种情况,SDK只暴露了CBC模式,客户非要国密。最后我搞了个SM4-CBC + HMAC-SHA256的组合方案,既能满足合规,又能保证完整性。

来看个用Bouncy Castle实现SM4-GCM的例子:

// SM4-GCM 加密示例(Java,使用BouncyCastle)
Security.addProvider(new BouncyCastleProvider());

public static byte[] encryptSm4Gcm(byte[] plaintext, byte[] key) throws Exception {
    byte[] iv = new byte[12];
    SecureRandom.getInstanceStrong().nextBytes(iv);
    
    Cipher cipher = Cipher.getInstance("SM4/GCM/NoPadding", "BC");
    GCMParameterSpec spec = new GCMParameterSpec(128, iv);
    cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "SM4"), spec);
    
    byte[] ciphertext = cipher.doFinal(plaintext);
    return ByteBuffer.allocate(iv.length + ciphertext.length)
            .put(iv)
            .put(ciphertext)
            .array();
}

有没有发现和AES-GCM的代码几乎一样?对,就是加解密算法换了个名字,其他套路完全一样。这也是我为什么说“搞懂了AES,SM4就上手了”。

⚠ 密钥长度陷阱

SM4密钥固定128位(16字节)。AES支持128/192/256位。有时候团队把AES的256位密钥直接套到SM4上,肯定会报错。我记得有一个项目上线前一天发现这个问题,所有人熬夜重新生成密钥。这种低级错误,越早发现越好。

4.4 三种模式的核心区别对比

维度 AES-GCM RSA-OAEP SM4(国密)
加密类型 对称加密 非对称加密 对称加密
密钥长度 128/192/256位 2048/4096位 128位
性能 极快(硬件加速) 慢(非对称运算) 快(国产芯片加速)
数据长度限制 无实际限制 最大190字节(2048位密钥) 无实际限制
认证能力 内置(GCM模式) 无(需额外签名) 内置(GCM模式)
合规场景 非国密敏感 通用场景 国内合规必选
四种模式选型决策树 业务需求:字段加密 能否接受可逆加密? 是否为性能敏感场景? 是否有合规要求? → AES-GCM → AES-GCM → 国密 SM4 是否为传统RSA改写? 混合加密场景? 非对称要求? → RSA-OAEP AES-GCM+RSA → RSA-OAEP 选型核心准则 无合规要求 → 优先AES-GCM;有合规要求 → 国密SM4;密钥分发场景 → RSA-OAEP

4.5 避坑总结

说了这么多,总结几个我踩过或见过别人踩过的坑:

  1. IV/Nonce重复使用:GCM模式下这是死罪。我曾经在压测环境里踩过这个坑,两个线程用了同一个IV,直接破功。
  2. 密钥硬编码:源码里写死密钥,上生产就忘改。我接手过一个老旧系统,SM4密钥写死在代码里,跑了三年没换过。这等于把门锁的钥匙挂在门上。
  3. 误用ECB模式:我就问一句,ECB模式下,加密后的数据能看出明文规律吗?能。所以别用ECB。
  4. SM4的GCM支持:不是所有国密SDK都实现了GCM模式。建议你提前确认SDK文档,免得拿到手才发现不支持。
  5. 混合加密组合不当:AES密钥用RSA加密时,一定要加padding。我见过直接用RSA/ECB/PKCS1Padding加密AES密钥的,安全性大打折扣。正确做法是RSA-OAEP。

💡 选型建议

  • 没有特殊要求:直接上AES-GCM,成熟、高效、安全
  • 合规要求(金融、政务):国密SM4-GCM
  • 密钥分发、小数据加密:RSA-OAEP
  • 大数据量机密:混合加密 AES-GCM + RSA-OAEP