国密SM4加密在智能电表通信中的Java实施完整指南:从密钥派生到DL/T 645报文加密的工程实践
正文
SEO摘要:本文深入讲解国密SM4在智能电表通信中的完整Java实现方案,涵盖密钥派生、CBC/GCM加解密封装、DL/T 645协议集成等核心技术。针对智慧能源管理系统中的抄表数据加密、远程控制指令保护等实际需求,提供可直接落地的工程代码和调试经验,帮助开发者快速构建合规、安全的物联网通信链路,满足政企、园区、公寓等场景的国密合规要求。
摘要:本文详细介绍了在智能电表通信中应用国密SM4加密算法的完整Java工程实现方案。文章首先阐述了智能电表通信安全的重要性及SM4算法的合规优势,随后系统讲解了从主密钥到会话密钥的派生流程,并提供了基于HMAC-SM3的密钥派生Java代码。接着,文章分别封装了SM4-CBC和SM4-GCM两种工作模式的加解密实现,CBC模式适用于普通数据帧加密,GCM模式则提供加密与认证一体化保护。针对电力行业标准DL/T 645协议,文章设计了加密帧结构并提供了完整的工程封装代码。最后,结合合众致达智能电表平台对接实践,总结了安全芯片差异、密钥有效期管理、地址域与数据区隔离等关键注意事项,并列举了BC版本兼容性、填充算法一致性、GCM认证失败等常见调试问题及解决方案。本文为智慧能源管理系统中的智能电表安全通信提供了一套可直接落地的技术方案。
一、前言:为什么智能电表通信必须考虑国密加密
在智慧能源管理系统大规模接入水电表设备的场景下,通信安全早已不是“可选项”。无论是集中器通过4G/NB-IoT将抄表数据回传云端,还是物联网平台向智能电表下发拉闸、参数更新等控制指令,一旦报文被截获或篡改,都可能带来计费错误、设备误动作乃至用户数据泄露。尤其在公寓用电安全管理与宿舍远程断电控制等高频操作场景中,指令被篡改可能导致误停电或恶意合闸,直接影响租户体验与用电安全。
国密 SM4是我国自主设计的分组密码算法,分组长度与密钥长度均为128位,采用32轮非线性迭代结构。相比AES-256,SM4在政务、能源、金融等受监管行业具有合规优势,并且已被纳入DL/T 645等电力行业协议的扩展安全框架。合众致达在多款NB-IoT/Cat.1智能电表与能源管理云的对接实践中,将SM4作为默认链路加密选项之一,以满足园区能耗数据采集、公寓预付费管理等客户对数据主权与通信安全的要求。
本文聚焦工程落地:从密钥派生、填充模式、加解密封装,到DL/T 645协议报文加密的完整Java实现,并给出实际部署中的踩坑备忘。
二、SM4 算法基础与适用场景
2.1 SM4核心参数
| 参数 | 值 | 说明 |
|---|---|---|
| 分组长度 | 128 bit(16字节) | 与AES-128相同 |
| 密钥长度 | 128 bit(16字节) | 不支持192/256位变长 |
| 迭代轮数 | 32轮 | 包含轮密钥扩展 |
| 工作模式 | ECB/CBC/CTR/GCM | 推荐CBC或GCM,避免裸ECB |
| 合规标准 | GM/T 0002-2012 | 国密局发布 |
2.2 智能电表通信中的典型加密需求
- 注册阶段:终端首次上线时,通过安全通道完成
设备密钥或会话密钥的协商/注入。 - 日常抄表:
集中器将 DL/T 645 协议数据帧加密后传输,防止中间人读取用电量、余额、事件记录。 - 控制指令:远程通断电、费率更新等指令必须做完整性与防重放保护,通常结合 SM4-CBC + HMAC-SM3 或 SM4-GCM 实现。在
预付费电表管理系统中,用户余额扣减与远程拉闸指令若被伪造,将直接造成经济损失。
2.3 为什么不直接用AES
AES-256在通用性能上占优,但国密 SM4在以下场景更具工程价值:
- 政企、高校、医院等客户强制要求国密合规;
- 与国产芯片、国产安全模块(如
SE 安全芯片)集成时,SM4硬加速更成熟; - 政府补贴类项目或招投标中,国密往往是评分项。
三、密钥派生:从主密钥到会话密钥
实际部署中,设备出厂时只写入一个主密钥(Master Key,MK),云端也不直接存储明文MK,而是存储经过PBKDF2或国密SM3 KDF派生后的密钥索引。每次会话建立时,通过设备ID、随机数、时间戳派生会话密钥(Session Key,SK)。
3.1 推荐派生流程
【建议配图:智能电表 SM4 密钥派生与报文加密流程】
3.1 推荐派生流程
下面是智能电表 SM4 密钥派生与报文加密的完整流程图:
流程说明:
- 注册阶段:设备端生成随机数 R1,与设备ID、版本号一起发送给平台
- 平台处理:平台生成随机数 R2 和时间戳 TS,使用主密钥 MK 和上下文信息派生会话密钥 SK
- 密钥同步:平台返回 SK 索引、R2 和 TS,设备端使用相同 KDF 计算得到相同的 SK
- 日常通信:后续所有 DL/T 645 数据帧都使用 SK 进行加密传输
3.2 Java实现:基于HMAC‑SM3的密钥派生
下面的代码实现了HKDF‑SM3风格的密钥派生,将主密钥扩展为128位会话密钥与128位初始向量(IV)。
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.crypto.macs.HMac;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.util.Arrays;
import java.nio.charset.StandardCharsets;
/**
* SM3‑HMAC 密钥派生:从主密钥 + 上下文派生会话密钥与IV
*/
public class Sm4KeyDerivation {
private static final int SM4_KEY_LEN = 16; // 128 bit
private static final int SM4_IV_LEN = 16; // 128 bit
public static byte[] extract(byte[] masterKey, byte[] salt) {
HMac hmac = new HMac(new SM3Digest());
hmac.init(new KeyParameter(salt));
hmac.update(masterKey, 0, masterKey.length);
byte[] prk = new byte[hmac.getMacSize()];
hmac.doFinal(prk, 0);
return prk;
}
public static byte[] expand(byte[] prk, byte[] info, int length) {
byte[] okm = new byte[length];
byte[] t = new byte[0];
int offset = 0;
int counter = 1;
while (offset < length) {
HMac hmac = new HMac(new SM3Digest());
hmac.init(new KeyParameter(prk));
hmac.update(t, 0, t.length);
hmac.update(info, 0, info.length);
hmac.update((byte) counter);
byte[] output = new byte[hmac.getMacSize()];
hmac.doFinal(output, 0);
t = output;
int copyLen = Math.min(SM3Digest.HASH_SIZE, length - offset);
System.arraycopy(t, 0, okm, offset, copyLen);
offset += copyLen;
counter++;
}
return okm;
}
public static SessionKey deriveSessionKey(String masterKeyHex, String deviceId, String r1, String r2, long timestamp) {
byte[] masterKey = hexToBytes(masterKeyHex);
byte[] salt = deviceId.getBytes(StandardCharsets.UTF_8);
byte[] prk = extract(masterKey, salt);
String context = String.join("|", deviceId, r1, r2, String.valueOf(timestamp));
byte[] info = context.getBytes(StandardCharsets.UTF_8);
byte[] okm = expand(prk, info, SM4_KEY_LEN + SM4_IV_LEN);
byte[] encKey = Arrays.copyOfRange(okm, 0, SM4_KEY_LEN);
byte[] iv = Arrays.copyOfRange(okm, SM4_KEY_LEN, SM4_KEY_LEN + SM4_IV_LEN);
return new SessionKey(bytesToHex(encKey), bytesToHex(iv));
}
// 辅助方法:hexToBytes / bytesToHex 省略,建议使用BouncyCastle的Hex工具
public static class SessionKey {
public final String keyHex;
public final String ivHex;
public SessionKey(String keyHex, String ivHex) {
this.keyHex = keyHex;
this.ivHex = ivHex;
}
}
}
关键点:info必须包含设备ID、双方随机数与时间戳,防止会话密钥被重放或跨设备复用。
四、SM4加解密Java封装:CBC模式与GCM模式
BouncyCastle(bcprov-jdk18on)是 Java 国密开发的事实标准。以下代码分别封装了 SM4/CBC/PKCS7Padding 与 SM4/GCM/NoPadding 两种模式,后者可一步完成加密与认证,更适合对控制指令做完整性与防篡改保护。
4.1 CBC 模式封装(适用于普通数据帧加密)
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.Security;
public class Sm4CryptoEngine {
static {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final String ALGORITHM_CBC = "SM4/CBC/PKCS7Padding";
public static byte[] cbcEncrypt(byte[] key, byte[] iv, byte[] plaintext) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM_CBC, "BC");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(plaintext);
}
public static byte[] cbcDecrypt(byte[] key, byte[] iv, byte[] ciphertext) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
IvParameterSpec ivSpec = new IvParameterSpec(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM_CBC, "BC");
cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
return cipher.doFinal(ciphertext);
}
public static void main(String[] args) throws Exception {
String keyHex = "0123456789abcdeffedcba9876543210";
String ivHex = "00000000000000000000000000000000";
String plain = "DL645_READ_DATA_20260701";
byte[] cipher = cbcEncrypt(Hex.decode(keyHex), Hex.decode(ivHex), plain.getBytes(StandardCharsets.UTF_8));
System.out.println("CBC 密文:" + Hex.toHexString(cipher));
byte[] recovered = cbcDecrypt(Hex.decode(keyHex), Hex.decode(ivHex), cipher);
System.out.println("解密结果:" + new String(recovered, StandardCharsets.UTF_8));
}
}
4.2 GCM 模式封装(适用于控制指令与完整性保护)
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import javax.crypto.AEADBadTagException;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.security.Security;
/**
* SM4-GCM:提供加密 + 认证标签(128-bit tag),适用于远程控制指令
*/
public class Sm4GcmEngine {
static {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private static final String ALGORITHM_GCM = "SM4/GCM/NoPadding";
private static final int GCM_TAG_LEN = 128; // bit
private static final int GCM_IV_LEN = 12; // byte,推荐 96-bit
public static byte[] gcmEncrypt(byte[] key, byte[] aad, byte[] plaintext) throws Exception {
byte[] iv = new byte[GCM_IV_LEN];
new SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance(ALGORITHM_GCM, "BC");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LEN, iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
if (aad != null && aad.length > 0) {
cipher.updateAAD(aad);
}
byte[] cipherBody = cipher.doFinal(plaintext); // 末尾含 16 字节 tag
// 输出格式:IV (12B) || Ciphertext + Tag
byte[] result = new byte[GCM_IV_LEN + cipherBody.length];
System.arraycopy(iv, 0, result, 0, GCM_IV_LEN);
System.arraycopy(cipherBody, 0, result, GCM_IV_LEN, cipherBody.length);
return result;
}
public static byte[] gcmDecrypt(byte[] key, byte[] aad, byte[] encryptedPackage) throws Exception {
if (encryptedPackage.length <= GCM_IV_LEN + GCM_TAG_LEN / 8) {
throw new IllegalArgumentException("密文长度不足");
}
byte[] iv = new byte[GCM_IV_LEN];
byte[] cipherBody = new byte[encryptedPackage.length - GCM_IV_LEN];
System.arraycopy(encryptedPackage, 0, iv, 0, GCM_IV_LEN);
System.arraycopy(encryptedPackage, GCM_IV_LEN, cipherBody, 0, cipherBody.length);
Cipher cipher = Cipher.getInstance(ALGORITHM_GCM, "BC");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LEN, iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "SM4");
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
if (aad != null && aad.length > 0) {
cipher.updateAAD(aad);
}
try {
return cipher.doFinal(cipherBody);
} catch (AEADBadTagException e) {
throw new SecurityException("GCM认证失败:数据可能被篡改或密钥错误", e);
}
}
public static void main(String[] args) throws Exception {
String keyHex = "0123456789abcdeffedcba9876543210";
byte[] aad = "CMD:TRIP:20260701".getBytes();
byte[] plain = "{"meterAddr":"123456789012","action":"open"}".getBytes();
byte[] pkg = gcmEncrypt(Hex.decode(keyHex), aad, plain);
System.out.println("GCM加密包:" + Hex.toHexString(pkg));
byte[] recovered = gcmDecrypt(Hex.decode(keyHex), aad, pkg);
System.out.println("GCM解密:" + new String(recovered));
}
}
五、DL/T 645报文加密的工程封装
DL/T 645是中国电能表通信的主流协议,常见控制码包括11H(读数据)、14H(写数据)等。将加密报文嵌入645帧时,需要把控制码后的用户数据区整体加密,并增加加密标识、报文长度等字段。
5.1 加密帧结构设计
【建议配图:DL/T 645 加密扩展帧结构示意图】
5.1 加密帧结构设计
下面是 DL/T 645 加密扩展帧的结构示意图:
字段说明:
| 字段 | 长度 | 说明 | 是否加密 |
|---|---|---|---|
| 起始符 | 1B | 68H,标识帧开始 | 否 |
| 地址域 | 6B | 电表地址,低前高后格式 | 否 |
| 控制码 | 1B | 原控制码 + 加密标志位(最高位为1表示加密) | 否 |
| 数据长度 | 1B | 后续加密数据区的字节数 N | 否 |
| 加密数据 | N B | 原数据经 SM4-CBC 加密后的密文 | 是 |
| 校验码 | 1B | 从地址域到数据区末的累加和,取低8位 | 否 |
| 结束符 | 1B | 16H,标识帧结束 | 否 |
关键点:
- 地址域不加密:确保集中器或网关能正确路由报文
- 加密标志位:控制码最高位(0x80)表示该帧为加密帧
- 数据长度字段:指示加密数据区的实际长度,便于接收方解析
- 校验范围:校验码计算包含地址域、控制码、数据长度和加密数据区
5.2 645 帧加密/解密工具类
import org.bouncycastle.util.encoders.Hex;
/**
* DL/T 645 数据区加密封装(SM4‑CBC)
*/
public class Dl645CipherFrame {
private static final byte FRAME_HEAD = 0x68;
private static final byte FRAME_TAIL = 0x16;
private static final byte ENCRYPT_FLAG = (byte) 0x80; // 控制码最高位表示加密
/**
* 构造加密帧
* @param meterAddr 6 字节电表地址(已按 645 格式低前高后)
* @param controlCode 原始控制码
* @param plaintextData 明文数据区(不含地址、控制码)
* @param key 16 字节会话密钥
* @param iv 16 字节 IV
*/
public static byte[] buildEncryptedFrame(byte[] meterAddr, byte controlCode,
byte[] plaintextData, byte[] key, byte[] iv) throws Exception {
byte[] cipherBody = Sm4CryptoEngine.cbcEncrypt(key, iv, plaintextData);
int dataLen = cipherBody.length;
byte[] frame = new byte[1 + 6 + 1 + 1 + dataLen + 1 + 1];
int pos = 0;
frame[pos++] = FRAME_HEAD;
System.arraycopy(meterAddr, 0, frame, pos, 6);
pos += 6;
frame[pos++] = (byte) (controlCode | ENCRYPT_FLAG);
frame[pos++] = (byte) (dataLen & 0xFF);
System.arraycopy(cipherBody, 0, frame, pos, dataLen);
pos += dataLen;
// 校验:从地址域到数据区末的累加和,取低8位
byte cs = 0;
for (int i = 1; i < pos; i++) {
cs += frame[i];
}
frame[pos++] = cs;
frame[pos++] = FRAME_TAIL;
return frame;
}
public static byte[] parseEncryptedFrame(byte[] frame, byte[] key, byte[] iv) throws Exception {
if (frame.length < 10 || frame[0] != FRAME_HEAD || frame[frame.length - 1] != FRAME_TAIL) {
throw new IllegalArgumentException("非法645帧");
}
int dataLen = frame[8] & 0xFF;
byte[] cipherBody = new byte[dataLen];
System.arraycopy(frame, 9, cipherBody, 0, dataLen);
return Sm4CryptoEngine.cbcDecrypt(key, iv, cipherBody);
}
public static void main(String[] args) throws Exception {
byte[] addr = new byte[]{0x12, 0x34, 0x56, 0x78, (byte) 0x90, 0x12};
byte ctrl = 0x11;
byte[] plain = new byte[]{0x00, 0x00, 0x00, 0x00}; // 读数据:数据标识
String keyHex = "0123456789abcdeffedcba9876543210";
String ivHex = "00000000000000000000000000000000";
byte[] frame = buildEncryptedFrame(addr, ctrl, plain, Hex.decode(keyHex), Hex.decode(ivHex));
System.out.println("加密帧:" + Hex.toHexString(frame));
}
}
六、与合众致达智能电表平台对接的注意事项
在合众致达智能电表与能源管理云的对接实践中,覆盖了从园区能耗数据采集到公寓预付费管理的多种业务场景,我们发现以下工程细节会显著影响上线成功率:
-
安全芯片与软件实现差异:部分电表内置
SE 安全芯片,SM4运算在芯片内完成;云端用Bouncycastle软件实现。需要确认双方对PKCS#7 Padding、IV处理、字节序的定义完全一致,否则首包解密就会失败。 -
会话密钥有效期:建议在平台侧设置
SK 过期时间(如24小时),并通过心跳或每次抄表前的认证握手更新。过期不更新的密钥容易被重放攻击利用。 -
地址域与数据区隔离:DL/T 645的地址域
不能加密,否则集中器或网关无法完成路由。只对控制码之后的数据区加密即可。 -
异常帧处理:加密帧在无线传输中可能被截断,平台侧应优先校验帧头帧尾与长度字段,再调用解密,避免无效报文消耗SM4算力。
七、常见踩坑(调试备忘)
-
BC 版本不兼容导致“No such algorithm: SM4/CBC/PKCS7Padding”
- 请确认使用
bcprov-jdk18on或bcprov-jdk15on1.70+ 版本,并在代码中显式Security.addProvider(new BouncyCastleProvider())。
- 请确认使用
-
CBC 模式解密后末尾乱码
- 通常是因为填充算法不一致。BouncyCastle 默认使用
PKCS7Padding,若设备端使用ZeroPadding或NoPadding,需统一规范。
// ❌ 错误示例:服务端用PKCS7Padding,设备端用ZeroPadding // 设备端加密(ZeroPadding) Cipher cipherDevice = Cipher.getInstance("SM4/CBC/ZeroPadding", "BC"); cipherDevice.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted = cipherDevice.doFinal(plaintext); // 服务端解密(PKCS7Padding) Cipher cipherServer = Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC"); cipherServer.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] decrypted = cipherServer.doFinal(encrypted); // 可能抛出异常或末尾乱码 // ✅ 正确示例:两端统一使用PKCS7Padding // 设备端和服务端都使用相同的填充算法 Cipher cipher = Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC"); cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); byte[] encrypted = cipher.doFinal(plaintext); - 通常是因为填充算法不一致。BouncyCastle 默认使用
-
GCM 模式"AEADBadTagException"
- 先检查 AAD 是否完全一致;再检查 IV 是否被错误地包含在认证计算中;最后确认密文未被截断。
-
Hex 字符串与 byte 数组混淆
- 32 个 Hex 字符才等于 16 字节 SM4 密钥。若把 16 个 ASCII 字符直接当密钥,长度只有 8 字节,会报
InvalidKeyException。
// ❌ 错误示例:将16个ASCII字符直接当作16字节密钥 String wrongKey = "0123456789abcdef"; // 16个字符,但每个字符是1字节 byte[] keyBytes = wrongKey.getBytes(StandardCharsets.UTF_8); // keyBytes.length = 16,但这是16个ASCII字符,不是16字节的SM4密钥 SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "SM4"); // 会报InvalidKeyException // ✅ 正确示例:使用32个十六进制字符表示16字节密钥 String correctKeyHex = "0123456789abcdeffedcba9876543210"; // 32个十六进制字符 byte[] keyBytes = Hex.decode(correctKeyHex); // 使用BouncyCastle的Hex工具 // keyBytes.length = 16,符合SM4密钥长度要求 SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "SM4"); // 正常使用 // 验证:32个十六进制字符 = 16字节 System.out.println("Hex长度:" + correctKeyHex.length()); // 32 System.out.println("字节长度:" + keyBytes.length); // 16 - 32 个 Hex 字符才等于 16 字节 SM4 密钥。若把 16 个 ASCII 字符直接当密钥,长度只有 8 字节,会报
-
DL/T 645 地址域高低位反转
- 645 协议地址域为低前高后,而加密函数只认原始字节序列。加密前务必确认地址域已按协议规范组织,不要把显示地址直接填入。
—## 八、总结
本文从密钥派生、SM4-CBC/GCM封装,到DL/T 645加密帧构造,给出了一套可直接落地的Java工程实现。对于智慧能源管理系统中涉及智能电表安全通信的场景,国密SM4不仅是合规要求,更是构建可信任物联网链路的基础能力。合众致达智能电表已在多个公寓用电安全管理与园区能耗数据采集项目中落地SM4加密通信,后续文章还会分享MQTT over TLS + SM4混合链路、以及非侵入式负荷监测数据加密采集方案,欢迎持续关注。
每周一/三/五更新,关注专栏获取更多技术分享
代码块清单
- 代码块1(约45行,Java):
SM3-HMAC密钥派生,从主密钥 + 上下文生成会话密钥与 IV - 代码块2(约50行,Java):
SM4-CBC加解密封装,适用于普通数据帧加密 - 代码块3(约60行,Java):
SM4-GCM加解密封装,适用于控制指令完整性与认证保护 - 代码块4(约55行,Java):
DL/T 645数据区加密帧构造与解析
九、技术架构图
9.1 BouncyCastle SM4 在 Java 加密体系中的位置
架构说明:
- Java 安全体系:JCA 提供标准的加密服务接口,通过 Provider 机制支持多种加密实现
- BouncyCastle Provider:作为第三方 Provider 注册到 JVM,提供国密算法实现
- SM4 算法支持 CBC、GCM、ECB 等多种工作模式
- SM3 哈希算法用于 HMAC-SM3 密钥派生
- 应用层调用:通过标准 JCA API 使用 BouncyCastle 提供的国密算法服务
Cipher.getInstance("SM4/CBC/PKCS7Padding", "BC")获取 SM4 加密实例- 自定义
Sm4KeyDerivation类基于 HMAC-SM3 实现密钥派生 Dl645CipherFrame封装 DL/T 645 帧的加密构造逻辑
依赖关系:
<!-- Maven 依赖 -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78</version>
</dependency>
初始化代码:
// 注册 BouncyCastle Provider
static {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
更多推荐


所有评论(0)