“对称加密算法、非对称加密算法与国密加解密”这几组词放在一起,乍看像是密码学教材目录,但真正做项目的人都知道,它们就是每天和数据安全贴身肉搏的三件武器。我最早被这套东西毒打,是在做支付类接口对接的时候。当时文档里写着“报文先用SM4加密,再用SM2签名”,我对着这两个名字完全懵了,脑子里全是AES和RSA,对SM系列几乎没有概念。后来把对称加密、非对称加密到国密加解密这条线完整梳理了一遍,才发现它们不是三门独立课程,而是同一套安全体系里分工明确的三个角色。
这篇内容适合刚接触加密的新手,也适合工作中有过“密钥怎么管理”“模式怎么配”“为什么验签老失败”这类困惑的开发者。读完你可以搞清楚三类算法各自解决什么问题,为什么项目里经常多个算法配合使用,以及国密方案落地时最容易在哪些环节翻车。
1. 一开始,我是怎么被加密算法折腾的
1.1 一次支付接口对接事故
那段时间我在做某开放平台的支付接口对接,对方要求所有报文字段用国密SM4加密,敏感字段用SM2做数字签名。起初我以为只是换一组算法名称的事,只要调库就能过,结果踩了连环坑。
先是填充方式不一致,我方用PKCS7Padding,对方服务端用的却是ISO10126Padding,解密时直接抛异常。接着是IV的处理,我图省事写死了一个全零向量,对方网关安全校验直接拒绝。最后是SM2签名格式,有的库输出ASN.1 DER格式,有的库输出原始的r加s拼接格式,不转换根本验不过。这组问题花了我整整两天时间。
说老实话,问题不在算法本身有多复杂,而是我对加密算法体系缺少一个完整的框架。对于做业务系统的开发者来说,真正需要处理的安全模块并没有想象中那么玄学。你要掌握的其实就是三条线:负责批量数据机密性的对称加密,负责密钥分发和数字身份验证的非对称加密,以及专门应对国内合规场景的一组国密算法。三者之间不是替代关系,而是合作分工的关系。
1.2 三条主线的关系:它们到底在解决什么问题
我用一个特别生活化的模型来理解它们。
假设你要给别人寄一个带锁的箱子。对称加密就是你和对方共同持有同一把钥匙,锁箱子和开箱子都用这把钥匙。问题是,这把钥匙怎么安全地交给对方?如果走快递,钥匙在路上的时候被人复制了,后面所有箱子都是白锁。非对称加密是另一种锁,箱子上有两把不同的钥匙:公钥只管锁,私钥只管开。任何人都可以拿公钥往箱子上加锁,但只有持有私钥的人才能打开,钥匙分发问题就解决了。
可公钥算法的缺点是计算量大,直接加密大数据非常慢。所以实际的工程方案往往是混合加密:先用非对称加密安全地传递一个临时的对称密钥,之后所有业务数据都用对称加密处理。国密算法做的事情,是把前面提到的几个环节用统一的国产标准重新实现了一遍:SM4对标对称加密,SM3对标哈希摘要,SM2对标非对称加密和签名。由于在政务、金融、能源等行业系统里覆盖得比较广,很多项目会要求同时支持国际算法和国密算法,这就需要你同时理解两套体系的参数、模式和工具链。
2. 对称加密算法:速度最快,但密钥分发是硬伤
2.1 AES的加密过程到底做了什么
对称加密的核心要求是:使用同一个密钥完成加密和解密。AES(高级加密标准)是目前最主流的分组对称加密算法。所谓分组,就是先把明文切成固定大小的块,AES每个块是128bit,也就是16字节。密钥长度可以选择128、192、256bit,密钥越长,抗暴力破解能力越强。在安全要求比较高的场景里,我一般直接上AES-256。
AES的内部结构可以看作一个多轮搅拌机。每一轮都对当前分组依次执行字节代换、行移位、列混合、轮密钥加这四种变换。字节代换是通过那张固定的S盒把每个字节映射成另一个字节,引入非线性;行移位和列混合负责把信息快速扩散到整个分组;轮密钥加则是把本轮扩展出的子密钥和分组数据做异或。128位密钥的AES会重复10轮,192位是12轮,256位是14轮。经过这么多轮迭代之后,明文中任意一个比特的变化都会像墨水滴进水里一样扩散到整个密文分组。
很多人刚学的时候会纠结“为什么不能只做一轮”?因为只做一轮的话,输入输出之间的关系太直接,统计规律太强,攻击者很容易找到关联。多轮迭代的目的就是把这个规律彻底搅乱。理解AES不需要你记住所有数学细节,但至少要知道一点:它的安全性建立在“被充分混淆和扩散”的密文之上,任何试图绕过轮数直接推导密钥的思路,都会撞上S盒的非线性特性。
2.2 分组模式怎么选:CBC、CTR、GCM
AES一次只能处理16字节,实际消息肯定不止16字节,所以需要定义怎么把多个分组串起来,这就是分组模式。
ECB模式是最简单也最不建议使用的。每个分组独立用同一把密钥加密,同样明文段落会产生同样密文段落。如果加密一张图片,即使密文没有泄露明文,轮廓信息依然能通过密文块之间的重复看出来。这在安全审计里是很严重的失分项。
CBC模式引入了一个IV(初始向量):第一个分组先和IV异或,再用密钥加密;后面的分组则和前一个密文分组异或后再加密。这样相同的明文块就不会产生相同的密文块,抵抗模式识别的能力明显增强。但CBC只管加密,不做完整性校验,攻击者可以翻转密文中的某些比特,导致解密出来的明文对应位置比特被翻转。
我在项目里最推荐的是GCM模式。GCM本质上是CTR模式加上GMAC认证,不仅能加密数据,还能生成一个认证标签,用来检测密文是否被篡改。等于把“保密性”和“完整性”一次性做完。放到转账业务里,金额字段被改动哪怕一个比特,认证标签就会对不上,接收方可以直接拒绝。CBC模式用起来也不难,但你必须另外再搭一套HMAC来做完整性校验,否则只保证加密不保证不可篡改。
| 模式 | 是否需要IV | 是否加密 | 是否认证 | 适用场景 | 备注 |
|---|---|---|---|---|---|
| ECB | 不需要 | 是 | 否 | 基本不推荐 | 明文模式泄露 |
| CBC | 需要,随机生成 | 是 | 否 | 单块传输,需配合HMAC | 填充敏感 |
| CTR | 需要,随机生成 | 是 | 否 | 流式加密 | 无填充但无认证 |
| GCM | 需要,随机生成 | 是 | 是 | 最推荐 | 加密和认证一步到位 |
这里还要补一个容易踩的坑:无论哪种模式,IV都绝对不能重用。尤其GCM模式,Nonce一旦重用,两段密文的认证密钥会被推导出来,攻击者可以伪造有效标签。很多线上事故都是因为随机数生成器种子相同或者有人图省事把Nonce写死导致的。
2.3 对称加密落地的三个细节
第一,填充问题。CBC模式要求明文长度必须刚好是分组长度的整数倍,不然就要做填充。最常见的填充是PKCS7Padding,补几个字节就填数值几。有些老系统还在用PKCS5Padding,实际上在AES场景下两者基本等价,但在某些语言库实现里会严格区分并报错,看起来是加解密失败,实际上只是填充名不一致。
第二,编码问题。加密的对象是字节数组,不是字符串。同一个字符串在不同字符集下字节完全不同。所有加解密统一使用UTF-8,不要用平台默认字符集。我们团队曾经在Windows开发环境上一切正常,部署到Linux服务器后解密出现乱码,最后定位到就是String.getBytes()默认用GBK和UTF-8的差异。从那以后,我在代码里强制规定所有字符到字节的转换必须显式指定charset。
第三,密钥管理。对称加密的速度虽快,但密钥分发和管理是最薄弱的一环。如果两侧使用同一个固定密钥,一旦密钥泄露,所有历史数据都可以被解密。常规的做法是引入密钥管理服务(KMS)或者硬件安全模块(HSM),密钥不直接出现在业务代码里。小团队哪怕没有KMS,也至少要做到环境变量注入而不是提交到代码仓库,并且建立定期轮换制度。用OpenSSL做文件加密时,也要养成加盐和用PBKDF2派生密钥的习惯,不要直接用裸密码做密钥:
# 用AES-256-CBC加密文件,必须带pbkdf2参数 openssl enc -aes-256-cbc -salt -pbkdf2 -iter 120000 -in plain.txt -out cipher.bin3. 非对称加密算法:公钥私钥解决密钥分发问题
3.1 RSA密钥生成与加解密原理
RSA是目前应用最广泛的非对称算法之一。它的安全性建立在大整数分解难题之上:两个大素数相乘很简单,但要反过来分解出这两个素数非常困难。生成密钥时,先随机挑选两个大素数p和q,计算它们的乘积n;再计算欧拉函数φ(n)=(p-1)(q-1);选择一个与φ(n)互质的公钥指数e,工程上通常固定为65537;最后计算私钥指数d,使得e和d的乘积模φ(n)等于1。公钥是n和e,私钥是n和d。加密时对明文消息m计算c等于m的e次方模n,解密时计算m等于c的d次方模n。
实际项目里不可能用很小的数字。RSA密钥长度至少2048bit,机密性要求高的场景建议3072或4096bit。因为n一旦小于一定位数,通过因数分解恢复私钥在计算上不再困难,整个机制就失去了意义。我在对接时经常看到有人为了响应速度把密钥改成1024bit,这是非常危险的操作,等于给攻击者留了一扇后门。
在Java里使用RSA时要注意算法字符串不能写裸的“RSA/ECB/PKCS1Padding”。我一般用“RSA/ECB/OAEPWithSHA-256AndMGF1Padding”,因为OAEP是概率性填充,每次加密同一个明文都会产生不同密文,可以避免攻击者通过密文比对猜测明文。PKCS1 v1.5填充虽然也能用,但已经暴露过安全性问题,新系统不应该再选它。
3.2 ECC和SM2是怎么把密钥变短的
RSA的问题是密钥太长,2048bit的密钥在证书、签名、传输时都显得很笨重。ECC(椭圆曲线密码)采用另一套数学结构:椭圆曲线上的点集构成一个阿贝尔群,给定基点G和私钥d,计算公钥Q等于d乘以G非常容易,但给定Q和G反推d却非常困难,这就是椭圆曲线离散对数问题。同样的安全强度下,ECC的密钥长度比RSA短很多,256bit左右的ECC密钥安全性大致能对标3072bit的RSA密钥。签名短、计算快、带宽占用少,这三点在移动端和物联网设备上非常实际。
SM2就是基于椭圆曲线体系设计的国密算法。和RSA相比,SM2不仅是密钥短,它的加密和签名流程还都嵌入了哈希函数和密钥派生函数,结构上更加现代。SM2可以完成加密、数字签名和密钥交换三类操作,在国密体系里基本承担了RSA加DH的全部工作。如果你已经理解了ECC公钥私钥的关系,理解SM2就顺理成章了。
3.3 项目里最常见的混合加密流程
在实际项目中,很少有谁全程只靠一种算法。最常见的流程是混合加密:发送方生成一个临时的会话密钥,然后用这个会话密钥配合对称加密算法加密业务数据;再用接收方的公钥加密这个会话密钥;接收方拿到密文后,先用自己的私钥解密出会话密钥,再用会话密钥解出业务数据。这样既保住了非对称加密的安全密钥分发能力,又利用对称加密的高效性完成了大数据量加解密。
有一次在对接某个数据交换平台时,对方文档给的正是这个结构。业务数据用SM4加密,SM4密钥用SM2加密,整体报文再用SM3做完整性摘要。当时我按照“先验证签名,再解出SM4密钥,再解业务数据”的顺序处理,很快就跑通了。TLS握手本质上也是混合加密,客户端用服务器公钥加密预主密钥,双方各自派生出会话密钥,后续记录层使用对称加密。
这里要特别强调一个顺序问题:很多人在联调时先解密数据再验签,被第三方平台拒绝后才改成先验签后解密。正确的顺序应该是先验签确认数据来源可信,再解密取数据。因为验签需要原文摘要,只有保留原始签名输入的明文才验得起来。一旦先解密,原始报文结构可能已经被转码或者加壳,验签就无从谈起了。
4. 国密加解密:SM2、SM3、SM4生态详解
4.1 SM4:与AES对标的分组加密算法
SM4是国密体系里的分组对称加密算法,分组长度128bit,密钥长度128bit,加密过程是32轮迭代。用大白话说,它的工作方式和AES类似,都属于用轮函数反复混淆的结构,所以使用习惯可以延续:依然有CBC、ECB、CTR等分组模式,依然要注意IV随机和密钥保护。SM4的最大特点是对资源占用不敏感,软件实现门槛低,在智能卡、密码机这类嵌入式环境里很常见。
在OpenSSL命令下,可以这样快速验证本机是否支持SM4:
openssl list -cipher-algorithms | grep -i sm4如果支持,用它加密文件时可以这样指定:
openssl enc -sm4-cbc -salt -pbkdf2 -in plain.txt -out cipher.bin不过要注意,SM4和AES在算法细节上完全不兼容,你不能拿AES的密钥格式直接往SM4里塞。在做国密改造时,经常出现“看起来调了SM4的接口,底层实际还在走AES实现”的情况。这种问题用抓包看协商套件,或者直接看日志里的算法标识就能发现。常见原因是底层Provider加载顺序不对,或者依赖库没有真正启用国密实现。
4.2 SM3:输出固定长度的密码杂凑算法
SM3是国密标准的哈希算法,输入任意长度消息,输出256bit摘要,主要用于完整性校验和数字签名辅助。它的内部结构和SHA-256有相似之处,都采用了消息扩展和压缩函数,但字节序、常数和轮函数不完全相同,所以SM3的摘要不能和SHA-256结果互相通用。如果项目没有国密需求,使用SHA-256通常更方便;但凡涉及到国内合规体系,SM3几乎是标配。可以用下面这条命令验证OpenSSL是否支持SM3:
openssl dgst -sm3 -hex message.txt在使用时,SM3最常见的坑是把它和“加盐”概念混淆。哈希算法不需要密钥,任何人在拿到同一段消息时都能算出同一个SM3值。因此它只负责证明数据有没有被改过,不负责证明“谁”生成的。要验证来源,必须配合数字签名:先用SM3算出摘要,再用SM2私钥对摘要签名。这是国密体系里的标准姿势。
在Java生态中,要启用SM3通常需要引入额外的开源密码库,因为JDK自带的Provider并不原生支持国密算法。很多项目在启动时遇到NoSuchAlgorithmException,多半就是没有正确加载Provider。排查时先检查依赖是否完整,再检查Provider是否在启动代码里注册过。这个问题我在多个项目里都见过,十个人有九个是忘掉了注册。
4.3 SM2:既能加密也能签名的椭圆曲线算法
SM2的完整密码流程比RSA复杂一些。以加密为例,它不只是把明文映射到椭圆曲线上的点,而是采用了一个混合结构:生成随机数k,计算椭圆曲线上的点C1等于k乘以G;再用接收方的公钥推导共享点,通过密钥派生函数KDF生成对称密钥流;用这个密钥流对明文做异或加密,得到C2;最后计算消息的校验值C3。最终密文是C1、C2、C3三段拼接。解密方拿到密文后,用自己的私钥恢复出共享点,再通过KDF重新派生密钥流,解出明文并校验C3。
这么设计的目的,是把“公钥加密”和“完整性校验”打包在一起。SM2签名与加密使用另一套流程:先对消息计算SM3摘要,生成随机数k,用k计算椭圆曲线点,再结合私钥推导出签名值。验签者用公钥、消息摘要和签名值恢复出验签点,比较结果是否一致。实际使用中不需要完全手写这些数学步骤,但至少要知道签名格式有差异:有的接口返回DER编码,有的返回原始的r和s拼接。跨系统对接时,必须先确认对方要哪种格式,否则验签失败会非常折磨人。
由于SM2可以同时承担加密和签名,有些项目图省事会把两个用途的密钥混用。严谨的做法是加密密钥和签名密钥分离。签名私钥用于证明身份,需要严格控制访问;加密私钥用于解密数据,可能被频繁调用。如果同一个私钥既做签名又做解密,一旦解密侧的数据被诱导,攻击者有可能通过某种密码学操作伪造出有效签名。这种密钥混用风险在密码学领域早被反复讨论过,国密改造中一定要把密钥用途分开。
4.4 国密算法改造要注意什么
把国际算法体系切到国密体系,不只是把接口里的“AES”替换成“SM4”这么简单。我经历过的国密切换通常会碰见几个层面的问题。
协议层面,以前用的是TLS握手和标准证书,国密方案要求使用国密套件和国密证书,证书还经常分成加密证书和签名证书两份。客户端和服务端协商过程不一样,很多现成的安全网关都需要配置独立的国密策略。库层面,默认的OpenSSL并不完整支持国密算法,你需要选择带国密补丁的分支版本,或者额外引入合规的密码库。应用层面,原有的密钥管理、日志监控、审计规则都需要跟着调整,某些安全设备也要检查是否支持国密密钥导入。
还有一个容易忽略的是“双算法支持”的需求。在很多业务系统里,过渡期新旧接口并行,旧客户端用国际算法,新客户端用国密算法。这意味着算法协商、密钥长度、证书体系都要做条件判断。这种兼容设计非常考验配置管理能力。我一般会在配置文件里定义算法优先级列表,并根据客户端标识动态选择套件,而不是在代码里写死。项目如果涉及多个语言栈,还要准备跨语言验证脚本,用固定测试向量同时跑不同语言实现,确保结果完全一致。
5. 实操经验:参数选型、故障排查与性能调优
5.1 项目里可以直接抄的选型表
我在接手一个新项目时,优先会问自己三个问题:数据在传输中需要什么机密性?消息是否可以被篡改或冒充?系统有没有特定的合规要求?根据回答,可以直接落到下面的常用选型:
| 使用场景 | 推荐算法与参数 | 说明 |
|---|---|---|
| 批量数据加密 | AES-256-GCM | 兼顾速度和完整性,避免只加密不认证 |
| 国密场景批量加密 | SM4,配合GCM模式 | 有国密需求时使用,同样需要认证 |
| 密钥交换/传输对称密钥 | RSA-2048及以上,或SM2 | 只加密少量密钥材料,不直接加密业务数据 |
| 数据摘要/完整性 | SHA-256或SM3 | 不用于防伪,必须配合签名 |
| 数字签名 | RSA-PSS,或SM2 | 签名长度短时优先SM2 |
| TLS证书 | ECDSA证书,或国密双证书 | 注意客户端兼容性 |
在这张表的基础上,还有一条黄金法则:不要自己设计加密协议。把常规方案组合起来用就足够安全了。任何“自己发明的加密方式”在专业攻击者面前往往都扛不住。找一个标准化的流程,比如在TLS里用ECDHE做密钥交换,用AES-GCM做记录层加密,用签名证书做身份认证,大概率不会出大问题。
5.2 高频报错和排查思路
联调过程中,我十次有八次遇到的不是算法本身的问题,而是参数选型不一致。下面这张表基本覆盖了最典型的报错:
| 现象 | 可能原因 | 排查建议 |
|---|---|---|
| BadPaddingException / decryption failed | 填充模式不一致,或密钥错误 | 先确认两边用同一种Padding,再检查密钥HEX是否完整匹配 |
| Illegal key size | JDK默认密码强度限制未解除 | 更新JDK版本,或在旧版本JDK中安装对应策略包 |
| IV长度不正确 | IV不是16字节,或模式没选对 | CBC和GCM都要求IV长度等于分组长度,即16字节 |
| SM3 MessageDigest not available | 国密Provider未加载 | 检查依赖引入和Provider注册调用 |
| 验签总失败 | 签名格式不一致 | 确认是DER格式还是r加s拼接格式,必要时写转换工具对比 |
| 解密出来是乱码 | 字符集不一致 | 双方统一UTF-8,不要让平台默认值参与决策 |
排查加密问题的时候,最有效的办法是构造一个可控的测试向量。找一个已知的明文、密钥、IV,先单独跑一遍算法,确认输出和预期一致,再把它放到业务流程里。我见过很多团队在联调时直接盯着线上日志,一看到解密失败就怀疑对方文档写得不对,其实往往是自己本地测试时没有固定随机数,导致结果不稳定。固定好测试向量,把变量控制在最小范围,问题定位会快很多。
5.3 性能优化和压测时的关键指标
加密算法的性能瓶颈常常被低估。RSA和SM2这类非对称算法虽然安全,但运算开销大。拿RSA来说,一次2048bit私钥操作大约需要毫秒级时间,而AES加密一兆字节数据往往只要亚毫秒时间。所以任何流量大的接口,都不应该用非对称算法直接加密消息体,而应该把非对称操作控制在一轮握手或者一轮密钥交换中。
在压测中我一般会观察几个指标:加解密操作的P99延迟、底层CPU指令集利用率、会话密钥的生成频率。如果服务部署在x86平台上,AES-NI指令集能显著提升AES性能,但前提是底层密码库确实启用了硬件加速。可以用下面这条命令看本机吞吐:
openssl speed -evp aes-256-gcmSM4在新版本OpenSSL中也开始逐步支持硬件加速,但很多老库仍然是纯软件实现,性能差距在数据量大时会很明显,这时要考虑升级依赖库版本。
另外一个常被忽略的点是,安全随机数的生成也可能成为瓶颈。安全随机数比普通随机数慢得多,如果每次新建密钥都从同一个随机源大量请求,有可能拖慢整体性能。工程上通常采用密钥池或会话密钥复用的方式降低开销,但复用必须遵守算法本身的安全约束。比如GCM的Nonce不能重用,否则安全强度会被击穿。理解安全边界和性能优化是两件事,这个分寸必须拿捏清楚。
6. 踩过坑之后的几点体会
从那次支付接口的连环事故到现在,我对加密算法体系的体会就是一句话:搞清楚“每个算法负责哪个环节”比背下一堆API更有用。对称加密负责又快又准地加密数据,非对称加密负责安全地传递密钥和确认身份,摘要与签名负责告诉别人“数据没被改过、来源可信”,国密则是把这套逻辑按统一标准重新武装了一遍。做方案的时候先问自己三个问题:密钥从哪来,算法有没有认证机制,签名和加密的顺序对不对。这三个问题想清楚,大部分项目里的加密方案不会跑偏。
最后再分享一个小技巧:联调时永远准备一份用来固定测试向量的JSON配置文件,里面记录明文、密钥、IV、模式、填充、预期密文。每次碰到问题,先跑这条基线用例;基线过了,再怀疑是参数传递或业务逻辑问题。这个习惯我在好几个项目里都验证过,确实是排查加密问题效率最高的手段。加密这种事,焦虑来源于未知,把每一步拆开验证,心里就有底了。