3、SM2加密与解密:加密流程、解密流程、CBC/ECB模式选择、实际代码演示
加密解密,说白了就是给数据上一把锁,再拿钥匙打开。SM2作为国密标准里的公钥密码算法,它的加解密流程和RSA不太一样。我个人习惯把SM2叫做“混合加密”——它用公钥加密一个临时生成的对称密钥,再用这个对称密钥去加密真正的数据。
嗯,这里要注意:SM2本身不是直接加密大块数据的。你想想看,公钥加密的计算量很大,如果直接拿它加密几兆的文件,性能会非常难看。所以实际工程里,SM2加密通常只处理一个32字节的对称密钥,剩下的活儿交给对称算法。
3.1 SM2加密流程
SM2加密,我拆成三步来讲。你在项目里写代码时,脑子里要时刻想着这三步。
- 生成临时密钥对:加密方随机生成一个临时私钥k,然后算出对应的临时公钥R = k * G。这个R会随密文一起发给对方。
- 计算共享密钥:用临时私钥k和接收方的公钥P,通过椭圆曲线点乘算出共享点S = k * P。然后从S中提取出密钥流,用于加密数据。
- 加密数据:用密钥流对明文进行异或操作,得到密文C。同时计算哈希值用于完整性校验。
最终输出的密文包含三部分:临时公钥R、密文C、以及哈希值。缺一不可。
核心要点:SM2加密不是直接加密,而是“密钥封装+数据加密”的组合。每次加密都会生成新的临时密钥对,所以同样的明文每次加密结果都不同。这叫概率加密,是安全的基本要求。
3.2 SM2解密流程
解密就简单多了。接收方拿到密文后,用自己的私钥d去解。
- 提取临时公钥:从密文中取出R。
- 计算共享密钥:用私钥d和临时公钥R,算出共享点S = d * R。这里用到了椭圆曲线的性质:d * R = d * (k * G) = k * (d * G) = k * P。加密方和解密方算出的S是同一个点。
- 解密数据:从S中提取密钥流,对密文C进行异或,还原出明文。
- 校验完整性:重新计算哈希值,和密文里带的哈希比对。不一致就说明数据被篡改了。
我曾经在项目里遇到过一个问题:解密出来的明文全是乱码。查了半天,发现是临时公钥的编码格式搞错了。SM2的临时公钥需要按X9.62标准压缩或非压缩编码,解码时如果格式不匹配,点乘结果就完全不对。
避坑指南:我曾经因为临时公钥的字节序问题,在联调时浪费了整整一天。SM2的坐标是大端序,和某些硬件平台的小端序不一样。跨平台传输时,一定要确认字节序。
3.3 CBC/ECB模式选择
这里要澄清一个常见的误解。SM2本身是公钥加密算法,它不直接支持CBC或ECB模式。这些模式是对称加密的概念。那为什么课程里要提这个?
因为实际使用SM2时,我们通常用它来加密一个对称密钥,然后用这个对称密钥配合AES或SM4去加密真正的数据。这时候,对称加密的模式选择就很重要了。
| 模式 | 特点 | 适用场景 | 我的建议 |
|---|---|---|---|
| ECB | 每个数据块独立加密,相同的明文块产生相同的密文块 | 加密单个短数据(如密钥本身) | 不要用于多块数据加密 |
| CBC | 每个数据块与前一个密文块异或后再加密,需要初始向量IV | 加密文件、数据库字段、网络传输数据 | 推荐用于大多数场景 |
| GCM | 带认证的加密模式,同时提供机密性和完整性 | 对安全要求极高的场景 | 如果性能允许,优先选GCM |
我个人习惯:如果只是加密一个32字节的SM2私钥或对称密钥,用ECB就够了。但如果要加密几百KB的业务数据,我会选CBC或GCM。ECB模式有个致命问题——它会把大象的轮廓暴露出来。你想想看,如果加密一张图片用ECB模式,密文里还能看出原图的轮廓,这显然不安全。
小技巧:SM2加密后的密文长度是固定的吗?不是。临时公钥的长度取决于椭圆曲线的参数。SM2推荐使用256位素域,临时公钥的非压缩编码是65字节(1字节标识+32字节x坐标+32字节y坐标)。加上密文和哈希,总长度会比明文多97字节左右。
3.4 实际代码演示
下面我用Java代码演示SM2的加密和解密。这里用的是Bouncy Castle库,它支持国密算法。嗯,要注意版本,BC 1.60以上才完整支持SM2。
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
import org.bouncycastle.crypto.params.ECKeyGenerationParameters;
import org.bouncycastle.crypto.params.ECPrivateKeyParameters;
import org.bouncycastle.crypto.params.ECPublicKeyParameters;
import org.bouncycastle.crypto.engines.SM2Engine;
import org.bouncycastle.crypto.params.ParametersWithRandom;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.math.ec.ECPoint;
import java.security.SecureRandom;
import java.security.Security;
public class SM2EncryptDemo {
public static void main(String[] args) throws Exception {
Security.addProvider(new BouncyCastleProvider());
// 1. 生成密钥对
ECKeyPairGenerator gen = new ECKeyPairGenerator();
ECKeyGenerationParameters keyParams = new ECKeyGenerationParameters(
SM2Util.SM2_DOMAIN_PARAMS, new SecureRandom());
gen.init(keyParams);
AsymmetricCipherKeyPair keyPair = gen.generateKeyPair();
ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();
// 2. 加密
SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2);
engine.init(true, new ParametersWithRandom(publicKey, new SecureRandom()));
byte[] plaintext = "Hello, SM2! 这是国密加密测试".getBytes("UTF-8");
byte[] ciphertext = engine.processBlock(plaintext, 0, plaintext.length);
System.out.println("密文长度: " + ciphertext.length + " 字节");
// 3. 解密
SM2Engine decryptEngine = new SM2Engine(SM2Engine.Mode.C1C3C2);
decryptEngine.init(false, privateKey);
byte[] decrypted = decryptEngine.processBlock(ciphertext, 0, ciphertext.length);
System.out.println("解密结果: " + new String(decrypted, "UTF-8"));
}
}
代码里有个细节:SM2Engine.Mode.C1C3C2。这是SM2密文的排列顺序。C1是临时公钥,C2是密文,C3是哈希。国密标准推荐C1C3C2,但有些旧实现用C1C2C3。我在对接第三方系统时遇到过这个问题——两边模式不一致,解密失败。所以联调前一定要确认好模式。
实战经验:SM2加密的性能瓶颈在椭圆曲线点乘。加密一次大约需要2-5毫秒(取决于硬件)。如果每秒要加密上千次,建议用预计算或硬件加速。我在一个支付系统中,把SM2加密放在异步线程里处理,避免阻塞主流程。
下面这张图展示了SM2加密解密的完整流程,你可以对照代码理解。
最后说一个我踩过的坑。SM2的密钥对生成后,私钥和公钥需要安全存储。私钥绝对不能明文存数据库,至少要用硬件安全模块或密码箱保护。公钥虽然可以公开,但也要防篡改——我在一个项目中,攻击者替换了数据库里的公钥,导致所有加密数据都能被攻击者解密。后来我们给公钥加了数字签名,才堵住这个漏洞。
推荐实践:SM2加密后的密文建议用Base64编码后再传输或存储。二进制数据在JSON、XML等文本协议里容易出问题。我一般用Base64.getEncoder().encodeToString(ciphertext),解码时再用Base64.getDecoder().decode()。