做跨平台数据对接的时候,最怕的就是加解密两边各写各的,明明算法名一样,结果一边能解一边解不开。最近正好做了一个支持Android和PC的DES、SM4加解密小工具,DES是老的金融兼容需求,SM4是国密标准要求,两套算法都得支持ECB模式。这个项目本身不大,但跨平台踩坑的密度极高——填充方式、密文编码、字符集、模式参数,任何一处没对齐,联调就是一场灾难。
这篇就围绕这个标题从头梳理一遍:核心方案怎么定、两个算法在ECB模式下要注意什么、Android和PC端的代码怎么落地、以及我实际联调中遇到的那些“看起来一样却不对”的问题。如果你正打算在手机端和桌面端打通加密数据,或者被国密算法SM4和旧系统DES的兼容问题卡住了,这篇可以直接照着参考。
1. 项目整体设计思路与方案选型
1.1 需求源头:为什么需要跨平台加解密
这个工具的起因很典型:一个业务系统需要同时服务移动端App和PC客户端,两端都要和服务端进行敏感数据交互。服务端是老系统,接口里约定的是DES ECB模式加密,而一些新的国密改造场景里又要求SM4的ECB模式。也就是说,同一个客户端,有时候要用DES解密老的报文,有时候又要用SM4处理新的数据。
这种“新旧并存”的需求在存量系统里非常普遍。老的接口用了十多年的DES,不太可能一次性全部切换;新的合规要求又明文规定必须用国密SM4。所以客户端不能只做一个算法,而是要把DES和SM4都做成通用能力,再通过统一入口去分发。
设计上我没有把两套逻辑写死,而是先定了一个抽象层:传入算法类型(DES或SM4)、模式(固定ECB)、明文或密文、密钥。内部根据算法分发到不同的实现。这样后续即使要加AES,或者把ECB换成CBC,改动面也只是在分发层加一个分支。
1.2 算法选型:为什么是DES和SM4,为什么用ECB
先说DES。DES是数据加密标准,密钥长度64位,实际有效位56位,分组长度64位。放到今天看安全性已经过时,从2018年起很多合规标准都不再允许新系统使用DES。但存量系统里DES存量非常大,尤其是一些金融历史接口、老版POS机通信协议、工业控制系统的配置指令,都还在用DES。所以做兼容适配时躲不开。
再说SM4。SM4是国密算法标准定义的对称分组密码算法,分组长度128位,密钥长度128位。它的设计公开、实现免费,在国内政务、金融、企业内部加密改造里已经是事实标准。SM4加解密速度和AES同一水平,但生态上早年不如AES普及,尤其跨语言、跨平台的时候,库的命名和调用方式差异不小,容易踩坑。
至于ECB模式,很多人一听就说“ECB不安全,你怎么还用”。这个观点在某些场景下是对的:ECB模式下相同的明文分组会产生相同的密文分组,明文结构容易被推断。但现实项目里,它仍然大量存在,原因主要有三个:
- 老系统协议固定死了ECB,你只能跟着做兼容;
- 短数据、随机性强的数据(比如令牌、一次性口令、密钥片段)用ECB不会暴露明显结构;
- ECB不需要IV,跨平台参数最少,联调最简单。
所以我的处理原则是:代码里支持ECB,但调用方必须明白它的边界。如果数据本身是高重复性的长文本,我会在文档里明确建议升级到CBC或者GCM。工具提供能力,使用方对安全性负责。
1.3 统一参数约定的重要性
跨平台加解密最核心的一件事不是算法本身,而是“约定一致”。算法变了谁都跑不出来,但约定和算法不一致,往往是“单端自测通过,联调就挂”。
工具在正式写代码前,我先定了下面这些约束:
- 密钥编码:统一UTF-8字符串形式的密钥,转字节数组时用UTF-8;
- 密钥长度:DES 8字节,SM4 16字节,且只接受刚好这个长度,避免填充导致混乱;
- 明文编码:统一UTF-8;
- 密文编码:统一Base64字符串,不在接口里传原始字节数组;
- 填充方式:统一PKCS5Padding(具体底层行为后面详讲);
- 字符集:所有字符串<->字节转换强制指定UTF-8,绝不让系统默认字符集参与。
这六个约束看着简单,但每一条对应一次踩坑。后面联调之所以顺利,就是因为这些约定在编码之前就已经白纸黑字写清楚了。
2. 算法原理与模式基础拆解
2.1 DES的算法结构与ECB行为
DES是Feistel结构的经典分组算法,分组大小64位。加密时把64位明文按位拆成左右两半,经过16轮迭代,每轮通过轮函数F结合48位子密钥做置换、替换和异或操作。解密则是同一套流程的逆向执行,密钥编排按相反顺序生成子密钥。
ECB(Electronic Codebook,电子密码本)是最简单的工作模式。它把明文按64位一组分割,每组独立用同一个密钥加密,各组之间没有任何关联。这个过程可以理解为每个人都有同一个固定印章,把一段话按固定长度切块,每块单独盖章——同样的内容盖出来的章完全一样。
ECB的好处是简单、可并行、无IV,坏处也正好暴露在这个“同样内容同样密文”上。如果明文有规律性,密文也会有规律性。经典例子是加密位图图片,ECB模式下图片轮廓依然清晰可见。所以ECB只适合短数据、随机数据的传输保护,不适合结构化长文本加密。
在Android的Java环境中,DES的ECB模式调用JCE(Java Cryptography Extension)原生支持,不需要额外引第三方库。只需要指定“DES/ECB/PKCS5Padding”这个转化字符串,系统就会按标准流程执行。
2.2 SM4算法结构与实现差异
SM4也是一个分组密码,分组长度128位,密钥长度128位,加解密结构是32轮非线性迭代。它的算法结构公开,不依赖任何专利授权,在国密体系中地位等同于国际算法里的AES。
SM4在Java原生JCE里并不直接支持(除非某些厂商的JDK增加了国密Provider)。所以常见方案是引入第三方库。目前我用过的方案有两套:
- Bouncy Castle(轻量级API):需要注册Provider,或者直接用它的轻量级API类;
- Hutool的SMUtil:适合做普通业务加密,内部封装了SM4和SM2等,对中小项目很友好。
我自己在这个工具里是用Bouncy Castle的轻量级API实现的,原因是它不依赖全局Provider状态,控制力更强。Hutool代码写起来更短,但在特殊场景(比如要严格控制Provider加载顺序)下灵活性略差。
SM4在ECB模式下的逻辑和DES在ECB下的逻辑完全一样:把128位明文分组,每组独立用密钥加密。只是分组关系和密钥长度不同。从工程实现的角度看,只要封装好了,两种算法的调用姿势几乎可以统一。
2.3 ECB、CBC与GCM模式的直观对比
为了说清为什么很多场合还要保留ECB,而又为什么不建议新系统用它,我做了个对比:
| 工作模式 | 是否需要IV | 分组间关联 | 安全强度 | 联调复杂度 |
|---|---|---|---|---|
| ECB | 不需要 | 无关联 | 最低,明文结构会泄露 | 最低,参数最少 |
| CBC | 需要 | 前后分组链式异或 | 较高,同一明文块也能产生不同密文 | 中,需要约定IV |
| GCM | 需要 | 融合认证 | 最高,同时提供加密和完整性校验 | 中高,需要约定IV和认证标签 |
如果只是内部工具、短随机数据、或者兼容老接口,ECB是可行的。但如果是对用户隐私数据做加密,或者传输的是结构化业务报文,我强烈建议用CBC起步,能上GCM更好。工具里保留ECB是兼容需求,不代表它是推荐项。
3. 跨平台实现细节与核心代码
3.1 Android端实现:Java + KeyGenerator统一入口
Android端的实现直接放在一个加密工具箱类里。统一入口方法接收算法名、原文和密钥字符串,内部判断走DES还是SM4。整体用Java实现,兼容性最好,不涉及Kotlin协程的额外依赖。
先看DES部分:
import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; import java.util.Base64; public class CryptoBox { private static final String DES_TRANSFORM = "DES/ECB/PKCS5Padding"; public static String desEncrypt(String plainText, String key) throws Exception { byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); if (keyBytes.length != 8) { throw new IllegalArgumentException("DES密钥长度必须为8字节"); } SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "DES"); Cipher cipher = Cipher.getInstance(DES_TRANSFORM); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); } public static String desDecrypt(String cipherText, String key) throws Exception { byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); if (keyBytes.length != 8) { throw new IllegalArgumentException("DES密钥长度必须为8字节"); } SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "DES"); Cipher cipher = Cipher.getInstance(DES_TRANSFORM); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(cipherText)); return new String(decrypted, StandardCharsets.UTF_8); } }注意几个点:
- 密钥必须强校验长度,DES密钥刚好8字节。如果密钥长度不对,JCE会直接抛InvalidKeyException,与其等到那里,不如在入口就报清楚。
Base64.getEncoder()在Android的API级别上兼容性还行,但老项目如果还停留API 26以下,可能需要换用android.util.Base64。- 明文和密文分别编码成Base64,避免字节数组在跨接口传递时被隐式转码。
3.2 SM4实现:基于Bouncy Castle轻量级API
SM4部分引入Bouncy Castle。Gradle依赖:
implementation 'org.bouncycastle:bcprov-jdk18on:1.78.1'这里我用的是Bouncy Castle的轻量级API,不抢系统Provider的位置,适合在多种运行环境下共存:
import org.bouncycastle.crypto.BlockCipher; import org.bouncycastle.crypto.engines.SM4Engine; import org.bouncycastle.crypto.modes.ECBBlockCipher; import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher; import org.bouncycastle.crypto.paddings.PKCS7Padding; import org.bouncycastle.crypto.params.KeyParameter; import java.nio.charset.StandardCharsets; import java.util.Base64; public class Sm4Crypto { private static final int SM4_KEY_LENGTH = 16; public static String sm4Encrypt(String plainText, String key) throws Exception { byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); if (keyBytes.length != SM4_KEY_LENGTH) { throw new IllegalArgumentException("SM4密钥长度必须为16字节"); } byte[] input = plainText.getBytes(StandardCharsets.UTF_8); PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( new ECBBlockCipher(new SM4Engine()), new PKCS7Padding()); cipher.init(true, new KeyParameter(keyBytes)); byte[] output = new byte[cipher.getOutputSize(input.length)]; int len = cipher.processBytes(input, 0, input.length, output, 0); len += cipher.doFinal(output, len); byte[] result = new byte[len]; System.arraycopy(output, 0, result, 0, len); return Base64.getEncoder().encodeToString(result); } public static String sm4Decrypt(String cipherText, String key) throws Exception { byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8); if (keyBytes.length != SM4_KEY_LENGTH) { throw new IllegalArgumentException("SM4密钥长度必须为16字节"); } byte[] input = Base64.getDecoder().decode(cipherText); PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher( new ECBBlockCipher(new SM4Engine()), new PKCS7Padding()); cipher.init(false, new KeyParameter(keyBytes)); byte[] output = new byte[cipher.getOutputSize(input.length)]; int len = cipher.processBytes(input, 0, input.length, output, 0); len += cipher.doFinal(output, len); byte[] result = new byte[len]; System.arraycopy(output, 0, result, 0, len); return new String(result, StandardCharsets.UTF_8); } }Bouncy Castle里SM4的ECB模式使用ECBBlockCipher包装SM4Engine,Padding用PKCS7Padding。从效果上它与JCE里的PKCS5Padding完全等价,因为JCE里所谓的PKCS5Padding在分组密码上实际就是PKCS7的逻辑。
注册Provider的方式也可以,但我不推荐在这类工具里做全局注册,因为部分SDK会修改系统JCE的Provider顺序,注册时机不对容易引起其他加密逻辑的NoSuchAlgorithmException。轻量级API把密码组件和Provider状态彻底隔离,省心。
3.3 PC端实现:C#版DES与SM4对照
PC端有几种语言场景:如果服务端是C#,客户端和服务端直接语言对齐是最容易的;如果PC端是Java桌面应用,那就和Android代码几乎完全复用。我这里以C#版为例写一套对照参考,因为C#和Java的加密API命名差异最大,联调时的坑也最多。
C#的DES加密通常用系统自带的DESCryptoServiceProvider:
using System.Security.Cryptography; using System.Text; public static string DesEncrypt(string plainText, string key) { using (var des = DES.Create()) { des.Key = Encoding.UTF8.GetBytes(key); des.Mode = CipherMode.ECB; des.Padding = PaddingMode.PKCS7; var encryptor = des.CreateEncryptor(); using (var ms = new MemoryStream()) { using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) { byte[] input = Encoding.UTF8.GetBytes(plainText); cs.Write(input, 0, input.Length); cs.FlushFinalBlock(); } return Convert.ToBase64String(ms.ToArray()); } } } public static string DesDecrypt(string cipherText, string key) { using (var des = DES.Create()) { des.Key = Encoding.UTF8.GetBytes(key); des.Mode = CipherMode.ECB; des.Padding = PaddingMode.PKCS7; var decryptor = des.CreateDecryptor(); using (var ms = new MemoryStream(Convert.FromBase64String(cipherText))) using (var cs = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) using (var reader = new StreamReader(cs, Encoding.UTF8)) { return reader.ReadToEnd(); } } }C#的SM4没有系统内置实现,同样用BouncyCastle的C#版本:
using Org.BouncyCastle.Crypto.Engines; using Org.BouncyCastle.Crypto.Modes; using Org.BouncyCastle.Crypto.Paddings; using Org.BouncyCastle.Crypto.Parameters; using System.Text; public static string Sm4Encrypt(string plainText, string key) { byte[] input = Encoding.UTF8.GetBytes(plainText); byte[] keyBytes = Encoding.UTF8.GetBytes(key); var cipher = new PaddedBufferedBlockCipher( new EcbBlockCipher(new SM4Engine()), new Pkcs7Padding()); cipher.Init(true, new KeyParameter(keyBytes)); byte[] output = new byte[cipher.GetOutputSize(input.Length)]; int len = cipher.ProcessBytes(input, 0, input.Length, output, 0); len += cipher.DoFinal(output, len); return Convert.ToBase64String(output, 0, len); } public static string Sm4Decrypt(string cipherText, string key) { byte[] input = Convert.FromBase64String(cipherText); byte[] keyBytes = Encoding.UTF8.GetBytes(key); var cipher = new PaddedBufferedBlockCipher( new EcbBlockCipher(new SM4Engine()), new Pkcs7Padding()); cipher.Init(false, new KeyParameter(keyBytes)); byte[] output = new byte[cipher.GetOutputSize(input.Length)]; int len = cipher.ProcessBytes(input, 0, input.Length, output, 0); len += cipher.DoFinal(output, len); return Encoding.UTF8.GetString(output, 0, len); }C#里有一个容易被忽略的差异:DES.Create()在不同系统上可能返回不同实现,但ECB+PKCS7的行为在.NET Framework和.NET Core/5+上是一致的,前提是显式指定Mode和Padding,不要依赖默认值。Java默认是ECB/PKCS5Padding,C#默认不是ECB,如果都不显式设置就会很隐蔽地不一致。
3.4 为什么SM4的Padding也叫PKCS7
很多初学者会困惑:Java里写PKCS5Padding,C#里写PKCS7Padding,两个都能和SM4配合吗?能。因为DES和SM4的分组长度是8字节或16字节,而PKCS5Padding标准定义的是针对8字节分组的填充;PKCS7Padding则通用地适配不同分组长度。JCE在DES/ECB/PKCS5Padding里实际执行的就是PKCS7的填充规则。所以两端一边写PKCS5,一边写PKCS7,跑出来结果一致。这个别名陷阱是跨端联调最容易引起恐慌的地方——看到名字不一样就以为对不上。
4. 跨端联调踩坑与一致性问题排查
4.1 最坑的三种不一致:填充、密钥、Base64
我个人联调时遇到的第一类问题,就是填充设置不一致。有一回Java端默认PCKS5Padding,C#端刚好设了PKCS7,因为别名等价所以没事。但有一次某人把C#端改成了PaddingMode.None,结果测试数据能过、数据一长就抛异常。原因很简单:输入长度不是块大小的整数倍时,没有Padding就无法加密;如果刚好是整数倍,NoPadding也能跑,造成一种“某些数据可以”的错觉。
密钥长度是第二类问题。DES要求8字节、SM4要求16字节,不能多也不能少。很多实现会静默截断或补零,导致同一把“密钥字符串”在两端得到不同的实际字节数组。例如“12345678”作为DES密钥是8字节没问题,但如果位的密钥是“1234567890”,A端可能直接报错,B端却悄悄截断用前8字节。这种情况最危险,因为B端自测全通过,联调怎么都对不上。
第三类是Base64。加密结果返回的是Base64字符串,但有的团队习惯用Hex,有的用Base64,接口文档没写清楚,结果密文传过去全是乱码。我的建议是:所有跨进程、跨语言传递的密文一律用Base64,因为它对URL友好度、存储友好度都更高;Hex只在调试时用,不要在正式接口里混用。
4.2 一个经典联调失败案例的定位过程
模拟一个场景:A端是Android,用Java的DES/ECB/PKCS5Padding加密,输出Base64;B端是C#桌面程序,用DES.Create()解密,一直报“Length of the data to decrypt is invalid”。排查时第一反应是密文数据被截断,但检查Base64解码后的字节长度没问题。接着怀疑是密文合解密的Padding自动去除失败,把C#端改为手动去掉填充字节,发现最后一位填充字节值竟然是0而不是常规的PKCS7填充字节。再回头查A端代码,发现有人在加密前手动做了补位,又设置了PKCS5Padding,等于填充了两次。解密端只处理一次填充,自然报错。
这个案例说明一个关键经验:不要让调用方自己整形填充行为。工具内部只管标准算法实现,外界只需传明文/密文和密钥。谁往工具外面私自加了补位、去位、转码,谁负责后续所有不一致问题。
4.3 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 单端自测通过,双端解密失败 | 填充、密钥长度或编码约定不一致 | 核对六项基础约定 |
| 抛出“Given final block not properly padded” | 密钥错误或密文被篡改 | 再用约定密钥解一次,更换密文样本 |
| DES报InvalidKeyException | 密钥长度不等于8字节 | 密钥串补齐或更换方案 |
| SM4报KeyParameter长度错误 | 密钥长度不等于16字节 | 检查UTF-8编码后的字节数 |
| Base64解码抛异常 | 密文包含URL转义字符或回车换行 | 解码前去掉空白字符,换用URL安全Base64 |
| 解密出来的字符串乱码 | 字节到字符串转换未指定UTF-8 | 全局强制使用UTF-8 |
| C#里报“Padding is invalid and cannot be removed” | 解密密钥错误或密文格式错误,且Padding模式不匹配 | 确认两侧Padding别名等价关系 |
4.4 调试技巧:先用固定测试向量打通
跨平台联调不要一开始就用真实业务数据。我的习惯是先准备一组固定的测试向量:比如明文“demo1234”、密钥“12345678”,预先在某个可信参考实现(比如命令行OpenSSL或在线工具)计算出期望Base64结果。然后两端分别跑,只要加密结果和期望值一致,就说明算法链路没问题;再互相解对方的结果,验证解密方向。
用标准向量比用真实数据有一个好处:问题发生时,你可以确定是代码问题还是数据问题。否则你用一段几千字的业务报文去排查,光是在密文堆里定位差异就够折腾的。准备工作看似多花五分钟,实际联调会顺利非常多。
5. 安全边界与工程化管理建议
5.1 明确告知使用方ECB的安全边界
工具里既然是“ECB模式”对外提供服务,那安全说明就必须跟上。我不是主张禁用ECB,而是主张在工具文档和代码注释里明确标出适用场景。好的做法是在统一入口的JavaDoc里写清楚:
- 仅适合长度短、随机性强、重复率低的数据;
- 禁止加密多行结构化文本、图片、数据库整表数据;
- 涉及个人敏感信息时,应改用CBC或GCM模式;
- 工具默认只提供ECB,不代表业务方可以无脑选它。
如果代码是给团队内部用的,最好是把这个说明写到仓库的README里,并配上对比示例。否则三个月后有人复用这段代码加密用户手机号,出了问题就是事故。
5.2 密钥管理不要让业务代码接触密钥串
这个工具本身不负责存储密钥,但调用方式上我要提一个工程经验:加解密函数不要直接暴露密钥字符串的SourceCode常量。尤其是Android端,密钥硬编码在代码里,APK一拆包就全部暴露。至少要做到密钥放到配置中心或服务端下发,客户端运行时获取;再退一步,也要用本地加密存储把密钥包一层。
PC端的密钥又常用配置文件存储,必须要设置权限限制,防止被同机其他用户读取。这些在你开发工具时未必能完全决定,但至少要在接口设计里为密钥动态传入留好口子,别图省事写死。
5.3 日志与错误信息也要脱敏
跨平台联调阶段最容易把密钥和明文打到日志里。我见过DEBUG日志直接打印密钥串、明文报文、解密后的完整内容,这在开发环境看着方便,一旦日志传到生产或第三方手里,等于加密白做。
建议在工具内部统一禁止日志输出明文内容。异常信息可以抛出去,但异常消息里也不要拼接密钥和明文。最简单的做法是:在统一入口处把传入的明文、密文、密钥都做长度脱敏,只打印前几位或后几位,例如只显示密钥的最后4位,方便区分是哪一套密钥在跑,又不泄漏完整内容。
5.4 抽象出统一的加解密门面
最后一个工程建议:无论Android端还是PC端,业务代码都不应该直接依赖Cipher、SM4Engine这些底层类。我会在两端各自封装一个门面接口,比如:
public interface CryptoService { String encrypt(String algorithm, String plainText, String key) throws Exception; String decrypt(String algorithm, String cipherText, String key) throws Exception; }业务层只关心“算法名+明文/密文+密钥”,不关心内部用的是JCE还是Bouncy Castle,也不用理会分组、填充这些细节。将来如果要在门面里加入AES、CBC、GCM支持,只改门面实现,业务代码不动。这个抽象看起来多写了一层,但跨语言维护的时候价值非常大——两端的门面接口签名完全一致,排查问题都不用两边翻代码,直接在接口对齐的层次上对比。
6. 常见问题排查与适配扩展
6.1 工具适配旧版本Android API的注意点
如果你的Android项目targetSdk还在比较低的范围,以下几个API兼容问题比较常见:
java.util.Base64在API 26才可用,旧项目需改用android.util.Base64,注意它和JDK Base64的编码策略不完全一致(特别是URL安全模式);StandardCharsets.UTF_8在API 19+可用,太旧的设备需用"UTF-8".getBytes()加显式异常处理;- Bouncy Castle依赖包体积不算小,如果App对包体积敏感,可以开启混淆去掉未使用的类;但SM4涉及的类不能混淆,需要在ProGuard规则里keep住相关包。
这些都算不上卡点,但提前知道能省排查时间。
6.2 SM4是否能在OpenSSL里打通
有一个大家常问的点:SM4能不能用OpenSSL做PC端验证?可以。从OpenSSL 1.1.1开始,SM4算法已经内置支持,但默认不支持ECB模式直接通过-sm4-ecb参数调用。实际要写一点配置或用命令行指定cipher name。不同发行版行为有差异,稳定性不如直接给PC端都装BouncyCastle来对齐。所以我建议PC端在开发和测试环境统一用BouncyCastle,不依赖OpenSSL的版本差异。
6.3 扩展到CBC/GCM的预留设计
虽然项目标题是ECB模式,但代码结构上,我在门面抽象层预留了模式参数。比如把transform字符串设计成由“算法、模式、填充”三段组合,而不是写死常量。这样后续增加CBC模式时,只要在Cipher.init时增加IvParameterSpec参数,就能在现有架构上平滑扩展。ECB不需要IV,而CBC需要16字节IV(对于SM4),两端的IV生成规则又得重新对齐——这个等到真正做的时候再细化。
7. 实操总结与个人体会
最后讲一下整个项目做下来我最大的感受:跨平台加解密从来不是算法本身难写,而是细节约定太难管。DES和SM4的ECB实现,Java、C#甚至Python写出来都不过几十行,难的是你对“密钥编码、填充模式、密文表示、字符集”这四个维度有没有全局一致的强制标准。我在这个项目里最大的收获就是把参数约定前置成了文档,一边写代码一边对照文档,联调时间缩短了一大半。
如果你也在做类似的事,我建议拿到需求先别急着写代码,花20分钟把两端的密钥规范、Padding别名、Base64策略、测试向量先对齐,后面就不会有那种“明明都是DES,就是解不开”的深夜排障时刻。
有一点我特别想强调:无论加解密工具做得多顺手,都不要无脑推广ECB模式。老项目不得不兼容的时候,用归用,但你要明确知道它的短板。新写的数据交换接口,尽量往CBC或者GCM方向走,哪怕工作量多一两天,长期来看安全账是划算的。这个工具我后续的扩展计划就是先把CBC模式补上,再抽空给密文加上完整性校验,这样才能真正覆盖更多的业务场景。