Java实现SM4国密算法:ECB与CBC模式实战详解
1. 项目概述与背景
最近在做一个需要数据安全传输的项目,甲方明确要求使用国密算法。在SM2、SM3、SM4这一套组合拳里,SM4作为对称加密算法,是处理批量数据加密的主力。网上关于SM4的资料不少,但要么是纯理论,要么代码片段零散,真正能跑通、并且能和其他标准工具(比如常用的在线加解密网站)互相验证的完整示例并不多。我自己在实现过程中,从算法模式选择到填充方式,再到与第三方工具的结果对齐,着实踩了不少坑。这篇文章,我就把用Java实现SM4的ECB和CBC两种最常用模式的过程,从原理到代码,再到如何用公开工具验证结果的完整链路,给大家彻底讲清楚。无论你是正在学习国密算法,还是项目中急需集成SM4,这篇实战总结应该都能帮你省下不少折腾的时间。
SM4算法是一种分组密码,分组长度和密钥长度均为128位。这意味着它一次处理128位(16字节)的数据,密钥也是128位。它的核心在于32轮的非线性迭代结构,安全性有充分保障。我们日常开发中,直接去实现这个轮函数意义不大,更多的是学会如何正确地使用它。这里的关键就在于“模式”和“填充”。ECB(电子密码本)模式简单直接,但安全性有缺陷;CBC(密码分组链接)模式通过引入初始化向量(IV)增强了安全性,是更推荐的选择。而填充(如PKCS5Padding/PKCS7Padding)是为了解决明文长度不是16字节整数倍的问题。把这些概念理清,代码实现就是水到渠成的事了。
2. 核心概念与模式选择解析
2.1 对称加密与SM4算法定位
在开始写代码之前,我们必须先搞清楚我们在用什么,以及为什么要这么选。对称加密,顾名思义,加密和解密用的是同一把钥匙。SM4就是这把“国标”的钥匙。它的定位和AES非常类似,都是块加密算法,但它是我国自主设计的商用密码标准。在金融、政务等对数据安全有强制合规要求的领域,使用SM4往往是必选项。
选择Java来实现,主要是因为其跨平台性和丰富的生态。通过JCE(Java密码学体系结构)或者一些国密算法提供商(如Bouncy Castle)的库,我们可以相对方便地调用这些算法。但“方便”只是相对的,魔鬼藏在细节里,比如JCE默认并不包含SM4的实现,这就需要我们引入额外的Provider。
2.2 ECB与CBC模式深度对比
这是本次实战的两个主角,它们的区别直接决定了加密结果的安全性和形态。
ECB模式是最基础的模式。它的工作方式非常直观:将明文分割成一个个独立的16字节块,然后用同一个密钥分别对每个块进行加密。解密过程亦然。这种模式的优点是简单、并行计算效率高。但它的致命缺点也同样明显:相同的明文块会被加密成相同的密文块。这意味着如果你的数据存在规律(比如一张纯色图片),在ECB加密后的密文中,这些规律依然会以某种形式暴露出来,无法隐藏明文的模式。因此,ECB模式一般不推荐用于直接加密有意义的数据,它更适合加密随机数据或作为其他更复杂模式的基础构件。
CBC模式则通过引入一个“链”的概念,解决了ECB的模式泄露问题。在CBC中,第一个明文块在加密前,会先与一个随机生成的“初始化向量”进行异或运算,然后再用密钥加密。得到的第一个密文块,又会作为“链”的一部分,与下一个明文块进行异或,之后再加密,如此循环。这个过程就像一环扣一环的链条。
这个设计带来了两个关键变化:第一,即使完全相同的明文,只要IV不同,加密出来的密文就完全不同,这极大地增强了安全性。第二,加密过程无法并行(因为下一块依赖上一块的密文),但解密过程可以并行。IV本身不需要保密,但必须不可预测,通常随密文一起传输。所以,在实际应用中,CBC是比ECB安全得多、也更常用的选择。
2.3 填充机制的必要性与PKCS7
SM4是分组密码,它要求输入的明文长度必须是16字节的整数倍。但现实中的数据长度是随机的。怎么办?这就需要填充。PKCS7是一种最常用的填充方案。它的规则很简单:如果需要填充N个字节,那么这N个字节的值就都设置为N。
举个例子,假设最后一块明文还差5个字节才满16字节,那么我们就填充5个字节,每个字节的值都是0x05。如果明文长度刚好是16字节的整数倍呢?按照PKCS7规则,我们需要额外填充一个完整的16字节块,每个字节值为0x10(即十进制16)。这样在解密时,通过读取最后一个字节的值,就能准确无误地移除填充。
在Java中,我们常听到PKCS5Padding。实际上在AES/SM4这种16字节分组的场景下,PKCS5Padding和PKCS7Padding是等同的。PKCS5原本是为8字节分组设计的(如DES),但被广泛沿用到了16字节分组上。在代码指定时,我们通常写PKCS5Padding,JCE或Bouncy Castle会正确处理。
3. 环境准备与依赖配置
3.1 JDK选择与密码学强度策略
首先确保你使用的是Java 8或以上版本。这里有一个关键点:Java默认的JCE策略文件可能对加密强度有限制。对于SM4这种128位密钥的算法,通常没问题,但如果你未来涉及更长的密钥(比如SM2),可能会遇到“非法密钥大小”的异常。为了避免后续麻烦,我建议一劳永逸地安装JCE无限强度权限策略文件。
具体做法是去Oracle官网(或你的JDK发行商处)下载对应你JDK版本的JCE无限强度权限策略包。下载后,里面会有local_policy.jar和US_export_policy.jar两个文件。用它们替换掉你JDK安装目录下%JAVA_HOME%/jre/lib/security/(对于JDK 8)或%JAVA_HOME%/conf/security/(对于JDK 9+)中的同名文件。替换前请备份原文件。这一步做完,就不用再担心密钥长度限制问题了。
3.2 引入Bouncy Castle密码学提供者
如前所述,标准的SunJCE Provider并不支持SM4。我们需要一个实现了国密算法的Provider。Bouncy Castle(BC)是一个开源的、应用广泛的密码学库,其轻量级版本(bcprov-jdk15on或更高)就包含了SM2、SM3、SM4的实现。这是我们的首选。
如果你是Maven项目,在pom.xml中添加如下依赖:
<dependency> <groupId>org.bouncycastle</groupId> <artifactId>bcprov-jdk15on</artifactId> <version>1.70</version> <!-- 请使用最新稳定版本 --> </dependency>如果你是Gradle项目,则添加:
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'添加依赖后,我们需要在代码中动态注册Bouncy Castle Provider,或者通过修改java.security配置文件静态注册。动态注册更灵活,也是我推荐的方式:
import org.bouncycastle.jce.provider.BouncyCastleProvider; import java.security.Security; public class SM4Demo { static { // 防止重复注册 if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Security.addProvider(new BouncyCastleProvider()); } } // ... 后续代码 }将这段静态初始化块放在你的工具类或主类中,确保在调用任何加密方法前,Provider已经就位。
注意:在Web应用或复杂多线程环境中,要确保Provider的注册只执行一次,避免并发问题。上面的
static块配合条件判断是一个简单有效的做法。
4. SM4 ECB模式实现详解
4.1 ECB加密核心代码实现
ECB模式没有IV,实现起来最为简单。我们目标是构建一个通用的工具方法。首先,我们需要生成或传入一个128位的密钥。密钥必须是16个字节。我们可以从一个字符串密码派生,但更安全的做法是使用密钥生成器。
import javax.crypto.Cipher; import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.SecureRandom; import java.util.Base64; public class SM4ECBUtil { /** * 生成一个随机的SM4密钥(128位) * @return 16字节的密钥字节数组 */ public static byte[] generateKey() throws Exception { // 指定算法为SM4 KeyGenerator kg = KeyGenerator.getInstance("SM4", BouncyCastleProvider.PROVIDER_NAME); // 初始化密钥长度为128位 kg.init(128, new SecureRandom()); SecretKey secretKey = kg.generateKey(); return secretKey.getEncoded(); } /** * SM4/ECB/PKCS5Padding 加密 * @param data 明文数据 * @param key 16字节的密钥 * @return Base64编码的密文字符串 */ public static String encryptECB(byte[] data, byte[] key) throws Exception { // 1. 根据字节数组生成密钥规范 SecretKeySpec secretKeySpec = new SecretKeySpec(key, "SM4"); // 2. 获取Cipher实例,指定算法/模式/填充,并指定Provider Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", BouncyCastleProvider.PROVIDER_NAME); // 3. 初始化为加密模式 cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec); // 4. 执行加密 byte[] encryptedData = cipher.doFinal(data); // 5. 为了方便传输和查看,转换为Base64字符串 return Base64.getEncoder().encodeToString(encryptedData); } }代码要点解析:
KeyGenerator.getInstance("SM4", ...):这里必须同时指定算法名"SM4"和提供者BouncyCastleProvider.PROVIDER_NAME(即"BC"),否则JCE会找不到该算法。Cipher.getInstance("SM4/ECB/PKCS5Padding", ...):这是完整的算法转换字符串。它明确告诉Cipher引擎:使用SM4算法,ECB模式,PKCS5填充。同样需要指定Provider。SecretKeySpec:这是一个简单的密钥规范类,用于将原始的字节数组密钥包装成JCE可以识别的Key对象。Base64:加密结果是二进制字节,直接转字符串会乱码。使用Base64编码是网络传输和文本存储的通用做法。这里用了Java 8+自带的java.util.Base64。
4.2 ECB解密与完整性验证
有加密自然要有解密。解密过程是加密的逆过程。
/** * SM4/ECB/PKCS5Padding 解密 * @param base64EncryptedData Base64编码的密文字符串 * @param key 16字节的密钥(必须与加密密钥相同) * @return 解密后的明文字节数组 */ public static byte[] decryptECB(String base64EncryptedData, byte[] key) throws Exception { // 1. 将Base64字符串解码为密文字节数组 byte[] encryptedData = Base64.getDecoder().decode(base64EncryptedData); // 2. 生成密钥规范 SecretKeySpec secretKeySpec = new SecretKeySpec(key, "SM4"); // 3. 获取Cipher实例 Cipher cipher = Cipher.getInstance("SM4/ECB/PKCS5Padding", BouncyCastleProvider.PROVIDER_NAME); // 4. 初始化为解密模式 cipher.init(Cipher.DECRYPT_MODE, secretKeySpec); // 5. 执行解密,doFinal方法会自动处理PKCS5Padding的去除 return cipher.doFinal(encryptedData); }关键点:解密时,Cipher对象在调用doFinal后,会自动识别并移除末尾的PKCS5填充。我们拿到的就是原始的明文数据。这是一个非常便利的特性,避免了手动去填充的麻烦和错误。
4.3 ECB模式实战测试与陷阱
我们来写一个简单的测试,并指出一个常见的“坑”。
public class TestSM4ECB { public static void main(String[] args) throws Exception { // 1. 生成密钥 byte[] key = SM4ECBUtil.generateKey(); System.out.println("密钥(Hex): " + bytesToHex(key)); // 2. 准备明文 String plainText = "Hello, SM4 ECB Mode! 这是中文测试。"; System.out.println("明文: " + plainText); // 3. 加密 String encryptedBase64 = SM4ECBUtil.encryptECB(plainText.getBytes("UTF-8"), key); System.out.println("ECB密文(Base64): " + encryptedBase64); // 4. 解密 byte[] decryptedBytes = SM4ECBUtil.decryptECB(encryptedBase64, key); String decryptedText = new String(decryptedBytes, "UTF-8"); System.out.println("解密后明文: " + decryptedText); // 5. 验证一致性 System.out.println("解密是否成功: " + plainText.equals(decryptedText)); } // 一个简单的字节数组转十六进制字符串的工具方法 public static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } }运行这个测试,你应该能看到加密解密成功,并且明文和解密后的文本一致。
踩坑记录:字符编码问题这是新手最容易出错的地方之一。注意
plainText.getBytes("UTF-8")和new String(decryptedBytes, "UTF-8")。我们加密的是字节数组,不是字符串。字符串到字节数组的转换必须指定明确的字符编码(如UTF-8)。如果在加密时用平台默认编码(比如getBytes()),而在解密时也用平台默认编码,当开发环境和部署环境默认编码不同时,就会导致解密出的字符串是乱码,即使密钥完全正确。最佳实践是始终显式指定统一的字符编码,如UTF-8。
5. SM4 CBC模式实现详解
5.1 IV的作用与安全生成
CBC模式的核心在于IV。IV需要满足两个条件:1. 长度必须是16字节(与分组长度相同);2. 必须是随机且不可预测的。每次加密都应该使用一个新的随机IV。IV不需要保密,可以公开传输,但必须保证解密方能够拿到同一个IV。
import javax.crypto.spec.IvParameterSpec; // ... 其他import public class SM4CBCUtil { /** * 生成一个随机的16字节IV */ public static byte[] generateIV() { byte[] iv = new byte[16]; // SM4分组大小是16字节 new SecureRandom().nextBytes(iv); return iv; } /** * SM4/CBC/PKCS5Padding 加密 * @param data 明文数据 * @param key 16字节密钥 * @param iv 16字节初始化向量 * @return Base64编码的密文字符串。实际应用中,IV需要和密文一起传给解密方。 */ public static String encryptCBC(byte[] data, byte[] key, byte[] iv) throws Exception { SecretKeySpec secretKeySpec = new SecretKeySpec(key, "SM4"); // 使用IvParameterSpec包装IV IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", BouncyCastleProvider.PROVIDER_NAME); // 初始化Cipher时传入IV参数 cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec); byte[] encryptedData = cipher.doFinal(data); return Base64.getEncoder().encodeToString(encryptedData); } }关键对象IvParameterSpec:这个类专门用来包装初始化向量。在cipher.init时,除了模式和密钥,第三个参数就是它。
5.2 CBC加密与解密完整实现
解密方需要同时拥有密钥、IV和密文。
/** * SM4/CBC/PKCS5Padding 解密 * @param base64EncryptedData Base64编码的密文 * @param key 16字节密钥 * @param iv 16字节初始化向量(必须与加密时使用的IV相同) * @return 解密后的明文字节数组 */ public static byte[] decryptCBC(String base64EncryptedData, byte[] key, byte[] iv) throws Exception { byte[] encryptedData = Base64.getDecoder().decode(base64EncryptedData); SecretKeySpec secretKeySpec = new SecretKeySpec(key, "SM4"); IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS5Padding", BouncyCastleProvider.PROVIDER_NAME); // 解密模式同样需要传入IV cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec); return cipher.doFinal(encryptedData); }5.3 CBC模式实战与IV传输方案
我们来测试CBC模式,并讨论IV的传输。一个常见的做法是将IV和密文拼接在一起传输。
public class TestSM4CBC { public static void main(String[] args) throws Exception { // 1. 生成密钥和IV byte[] key = SM4ECBUtil.generateKey(); // 复用之前的生成方法 byte[] iv = SM4CBCUtil.generateIV(); System.out.println("密钥(Hex): " + bytesToHex(key)); System.out.println("IV(Hex): " + bytesToHex(iv)); // 2. 准备明文 String plainText = "Hello, SM4 CBC Mode! 这是更安全的模式。"; System.out.println("明文: " + plainText); // 3. 加密 String encryptedBase64 = SM4CBCUtil.encryptCBC(plainText.getBytes("UTF-8"), key, iv); System.out.println("CBC密文(Base64): " + encryptedBase64); // 4. 模拟传输:将IV和密文组合。例如,IV放在密文前面,一起做Base64 byte[] combined = new byte[iv.length + Base64.getDecoder().decode(encryptedBase64).length]; System.arraycopy(iv, 0, combined, 0, iv.length); System.arraycopy(Base64.getDecoder().decode(encryptedBase64), 0, combined, iv.length, Base64.getDecoder().decode(encryptedBase64).length); String combinedBase64 = Base64.getEncoder().encodeToString(combined); System.out.println("IV+密文组合(Base64): " + combinedBase64); // 5. 模拟接收方:拆分IV和密文 byte[] combinedReceived = Base64.getDecoder().decode(combinedBase64); byte[] ivReceived = new byte[16]; byte[] cipherTextReceived = new byte[combinedReceived.length - 16]; System.arraycopy(combinedReceived, 0, ivReceived, 0, 16); System.arraycopy(combinedReceived, 16, cipherTextReceived, 0, cipherTextReceived.length); String cipherTextBase64Received = Base64.getEncoder().encodeToString(cipherTextReceived); // 6. 解密 byte[] decryptedBytes = SM4CBCUtil.decryptCBC(cipherTextBase64Received, key, ivReceived); String decryptedText = new String(decryptedBytes, "UTF-8"); System.out.println("解密后明文: " + decryptedText); System.out.println("解密是否成功: " + plainText.equals(decryptedText)); } // ... bytesToHex方法同上 }实操心得:IV的存储与传输上面演示了将IV和密文简单拼接的方法。在实际系统中,你需要确保解密方能可靠地区分出IV和密文。除了拼接,也可以将IV作为单独的字段(例如,另一个Base64字符串)和密文一起放在JSON或协议头中传输。绝对不要固定使用同一个IV,那会让CBC模式的安全性大打折扣。
6. 使用在线工具进行交叉验证
自己写的代码加密解密成功,不代表结果就是正确的。我们需要用公认的第三方工具进行交叉验证,确保我们的实现与标准算法完全一致。这是密码学编程中至关重要的一步。
6.1 验证思路与准备工作
验证的核心是“对齐参数”。我们必须保证双方使用的密钥、IV(如果是CBC)、明文、模式、填充方式完全一致。任何一项不同,结果都会天差地别。
- 固定测试数据:为了便于验证,我们不使用随机生成的密钥和IV,而是使用固定的、可复现的测试向量。
- 选择验证工具:搜索“SM4在线加密解密”,可以找到很多国内开发者提供的工具。选择一个界面清晰、能指定模式和填充的。例如,有些网站明确提供了SM4/ECB/PKCS5Padding和SM4/CBC/PKCS5Padding的选项。
- 数据格式:在线工具通常接受Hex(十六进制)或Base64格式的输入。我们代码中密钥和IV是字节数组,需要转换成Hex或Base64字符串。明文通常是文本。
6.2 ECB模式工具验证实战
我们设计一个固定的测试用例:
- 密钥(Hex):
0123456789abcdeffedcba9876543210(正好32个十六进制字符,代表16字节) - 明文:
Hello SM4! - 模式: ECB
- 填充: PKCS5Padding
第一步:用我们的Java代码计算密文。我们需要稍微修改一下测试代码,使用固定的密钥。
public class VerifyECB { public static void main(String[] args) throws Exception { // 固定的Hex密钥 String keyHex = "0123456789abcdeffedcba9876543210"; byte[] key = hexStringToByteArray(keyHex); // 需要实现hexStringToByteArray方法 String plainText = "Hello SM4!"; String encryptedBase64 = SM4ECBUtil.encryptECB(plainText.getBytes("UTF-8"), key); System.out.println("Java计算出的密文(Base64): " + encryptedBase64); // 也可以输出Hex格式,方便对比 byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64); System.out.println("Java计算出的密文(Hex): " + bytesToHex(encryptedBytes)); } // Hex字符串转字节数组 public static byte[] hexStringToByteArray(String s) { int len = s.length(); byte[] data = new byte[len / 2]; for (int i = 0; i < len; i += 2) { data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i+1), 16)); } return data; } // ... bytesToHex方法同上 }运行后,假设我们得到密文(Hex)为:d6f0c6cfb3c9b4a8a3f5e1c2d7b8a9b0(这是一个示例,实际值以运行结果为准)。
第二步:使用在线工具。
- 打开一个SM4在线加解密网站。
- 输入模式:
ECB。 - 输入密钥格式:
Hex,内容:0123456789abcdeffedcba9876543210。 - 输入明文:
Hello SM4!。 - 选择填充:
PKCS5Padding或PKCS7Padding。 - 点击加密。
第三步:对比结果。比较在线工具输出的密文(Hex格式)与我们Java代码计算出的密文(Hex格式)是否完全一致。如果一致,恭喜你,你的ECB模式实现是正确的!
6.3 CBC模式工具验证实战
CBC模式需要额外指定IV。
- 密钥(Hex):
0123456789abcdeffedcba9876543210 - IV(Hex):
1234567890abcdef1234567890abcdef(32个十六进制字符,16字节) - 明文:
Hello SM4 CBC! - 模式: CBC
- 填充: PKCS5Padding
第一步:Java代码计算。
public class VerifyCBC { public static void main(String[] args) throws Exception { String keyHex = "0123456789abcdeffedcba9876543210"; String ivHex = "1234567890abcdef1234567890abcdef"; byte[] key = hexStringToByteArray(keyHex); byte[] iv = hexStringToByteArray(ivHex); String plainText = "Hello SM4 CBC!"; String encryptedBase64 = SM4CBCUtil.encryptCBC(plainText.getBytes("UTF-8"), key, iv); byte[] encryptedBytes = Base64.getDecoder().decode(encryptedBase64); System.out.println("Java计算出的CBC密文(Hex): " + bytesToHex(encryptedBytes)); } // ... hexStringToByteArray和bytesToHex方法同上 }运行得到密文(Hex),例如:a1b2c3d4e5f678901234567890abcdef。
第二步:在线工具验证。
- 模式选择:
CBC。 - 密钥(Hex):
0123456789abcdeffedcba9876543210。 - IV(Hex):
1234567890abcdef1234567890abcdef。 - 明文:
Hello SM4 CBC!。 - 填充:
PKCS5/PKCS7。 - 加密。
第三步:对比密文Hex值。一致则说明CBC实现正确。
注意事项:有些在线工具可能默认使用
ZeroPadding或无填充,或者输入输出格式不同(比如要求密钥是Base64)。一定要仔细核对工具的所有选项,确保与你的代码参数完全匹配。这是验证成功的关键。
7. 常见问题排查与性能优化
7.1 异常处理与问题诊断
在集成过程中,你可能会遇到以下常见异常:
NoSuchAlgorithmException或NoSuchPaddingException- 原因:最可能的原因是Bouncy Castle Provider没有成功注册,或者算法转换字符串写错了。
- 排查:
- 检查
Security.addProvider是否执行,且没有抛出异常。 - 检查
Cipher.getInstance("SM4/ECB/PKCS5Padding", "BC")中的字符串是否拼写正确。模式ECB/CBC,填充PKCS5Padding,一个都不能错。 - 确认引入的Bouncy Castle Jar包在类路径中。
- 检查
InvalidKeyException或Illegal key size- 原因:密钥长度不对,或者因为JCE策略限制导致密钥强度不足。
- 排查:
- 打印密钥字节数组长度,SM4必须是16字节。
- 确认已安装JCE无限强度策略文件(见3.1节)。
IllegalBlockSizeException或BadPaddingException- 原因:通常在解密时发生。密文被篡改、密钥错误、IV错误或者填充损坏都会导致此异常。
- 排查:
- 确保解密使用的密钥和加密时完全一致。
- 对于CBC模式,确保IV完全一致。
- 确保密文在传输过程中没有被修改(比如Base64解码错误)。
- 确保加密和解密使用的模式和填充方式一致。
解密后是乱码
- 原因:几乎可以肯定是字符编码问题。
- 排查:严格检查加密时的
plainText.getBytes("UTF-8")和解密时的new String(decryptedBytes, "UTF-8"),确保编码一致且显式指定。
7.2 性能考量与最佳实践
Cipher对象复用:
Cipher对象的初始化(init方法)是比较耗时的操作。如果你的应用需要频繁加密解密,且密钥和模式固定,可以考虑将初始化好的Cipher对象缓存起来复用。但要注意线程安全,可以为每个线程创建独立的实例,或者使用ThreadLocal。大文件加密:对于大文件,不要一次性将全部数据读入内存调用
doFinal。应使用Cipher的update和doFinal方法进行分段处理。Cipher cipher = ... // 初始化 try (FileInputStream fis = new FileInputStream(inputFile); FileOutputStream fos = new FileOutputStream(outputFile); CipherOutputStream cos = new CipherOutputStream(fos, cipher)) { byte[] buffer = new byte[8192]; int len; while ((len = fis.read(buffer)) != -1) { cos.write(buffer, 0, len); } }使用
CipherOutputStream可以优雅地实现流式加密。密钥管理:硬编码密钥在代码中是极不安全的。生产环境中,密钥应来自安全的配置中心、密钥管理系统(如HashiCorp Vault)或硬件安全模块(HSM)。至少也要做到密钥与代码分离。
模式选择:重申一遍,优先使用CBC模式。对于需要认证的加密场景(确保密文未被篡改),可以考虑使用GCM等认证加密模式,但SM4的GCM实现可能需要寻找特定的Provider或自己实现,复杂度较高。
我个人在几个金融类项目中集成SM4的经验是,前期花时间做好与标准工具的交叉验证,能避免后期联调时大量的扯皮和排查工作。把密钥、IV、编码这些“琐事”用工具类封装好,定义清晰的接口,后续开发会顺畅很多。最后,密码学是门严谨的科学,差之毫厘,谬以千里,多测试、多验证永远没错。
