HarmonyOS ArkTS国密SM4_CBC加解密实现:原理、代码与异步优化
1. 项目概述:为什么要在ArkTS中实现SM4_CBC?
最近在做一个HarmonyOS应用,涉及到用户敏感数据的本地存储,比如一些配置信息和临时凭证。直接存明文肯定不行,用系统提供的加密API当然最省事,但这次的需求有点特殊:需要与一个已有的服务端系统进行数据交互,而对方使用的加密算法是国密SM4,并且是CBC模式。这就意味着,如果我只用HarmonyOS的通用加密接口,两端可能对不上,解密会出乱子。所以,我必须得在ArkTS这一侧,把SM4_CBC的加解密给完整地实现出来。
SM4作为国密算法,在金融、政务等领域应用越来越广,HarmonyOS作为国产操作系统的代表,其应用生态与国密算法的结合是一个很实际的需求点。CBC(Cipher Block Chaining)模式是分组密码中最常用的模式之一,它通过引入初始化向量(IV)和链式反馈,使得即使相同的明文,加密后也会得到不同的密文,安全性比ECB模式高得多。在ArkTS中实现它,不仅是为了完成手头的项目,更是为了打通HarmonyOS应用与现有国密体系服务端之间的数据安全通道。
这个实现过程,我会从最基础的算法原理讲起,然后一步步带你完成在ArkTS环境下的代码编写。最关键的是,我会给出 异步 和 同步 两种实现方式的完整代码,并对比它们的适用场景和性能差异。无论你是刚接触HarmonyOS开发,还是正在为数据加解密问题头疼,这篇内容都能给你一份可以直接“抄作业”的解决方案。
2. 核心原理与设计思路拆解
在动手写代码之前,我们必须先把SM4算法和CBC模式的工作原理吃透,这样才能理解后续每一个步骤和参数的意义,遇到问题也知道从哪里排查。
2.1 SM4算法与CBC模式核心机制
SM4是一种分组对称加密算法,分组长度和密钥长度都是128位(即16个字节)。你可以把它想象成一个高度复杂的“替换和移位”机器,它把16字节的明文数据块“打乱重排”很多轮(32轮),最终输出16字节的密文。这个“打乱”的规则,就由你的128位密钥来决定。
单独使用SM4对每个16字节块进行加密(即ECB模式)是有安全缺陷的,因为相同的明文块会加密成相同的密文块。这在加密一张图片或具有重复结构的数据时,密文仍然会保留明文的模式,容易被攻击者分析。
CBC模式就是为了解决这个问题而生的。它的核心思想是“链式”加密:
- 初始化向量(IV) :首先需要一个随机生成的、长度也为16字节的IV。这个IV不需要保密,但必须不可预测,通常每次加密都随机生成。
- 异或操作 :在加密第一个明文块之前,先将其与IV进行按位异或(XOR)操作。
- 加密 :将异或后的结果送入SM4算法进行加密,得到第一个密文块。
- 链式反馈 :在加密第二个明文块时,不再使用IV,而是将 第一个密文块 拿来与第二个明文块进行异或,然后再加密。以此类推,每一个明文块的加密,都依赖于前一个密文块。
解密过程则是这个过程的逆过程,需要用到相同的密钥和IV。
这种设计带来了一个关键特性: 即使两份明文完全一样,只要IV不同,产生的全部密文也会截然不同 。同时,密文中一个比特的错误,会影响后续两个数据块的解密,这在一定程度上提供了数据完整性校验。
2.2 在ArkTS中实现的设计考量
理解了原理,我们来看在ArkTS中实现的挑战和设计选择。
首要挑战:缺少现成的SM4算法库。 ArkTS/OpenHarmony的标准库目前没有直接提供SM4算法实现。因此,我们必须自己实现SM4的核心运算(S盒、轮函数等),或者寻找一个可靠的、可移植的纯TypeScript/JavaScript实现。
我的选择是采用一个经过社区验证的、轻量级的纯JS SM4实现。这样做的好处是:
- 零依赖 :不引入任何native模块或第三方npm包,纯ArkTS代码,兼容性最好。
- 代码透明可控 :所有算法逻辑一目了然,便于调试和定制。
- 体积小 :不会显著增加应用包大小。
第二个关键点:数据填充(Padding)。 SM4是分组算法,一次处理16字节。但我们的数据长度不可能总是16的整数倍。因此,在加密前,必须对明文进行填充,使其长度为16的倍数;解密后,再去除填充,恢复原始数据。
最常用的填充方案是PKCS#7。它的规则很简单:如果需要填充N个字节,那么每个填充字节的值就是N。例如,如果最后一个块差3个字节,就填充 0x03 0x03 0x03 。解密时,读取最后一个字节的值,就知道要移除最后几个字节。这种填充方式非常可靠,也是我们实现中将采用的方案。
第三个设计重点:异步与同步的抉择。 加解密运算,尤其是数据量较大时,是CPU密集型操作。如果在UI主线程(同步)中进行,可能会导致界面卡顿、响应迟缓。因此,提供异步接口是提升应用体验的关键。
- 同步实现 :代码简单直接,适用于加密小块数据(如一个密码、一个令牌),或者在不关心UI响应的后台任务中。
- 异步实现 :通过ArkTS的
TaskPool(任务池)将加解密计算抛到后台线程执行,完成后通过Promise或回调返回结果。这能保证UI流畅,适用于加密文件、大型缓存数据等场景。
我们的完整实现将同时包含这两种方式,并给出清晰的对比,让你能根据实际情况灵活选用。
3. 核心模块解析与代码实现
接下来,我们进入核心的代码实现环节。我会将整个工程拆解为几个关键模块,并附上详细的代码注释。
3.1 SM4算法核心类实现
首先,我们需要一个实现SM4基本运算的类。这里我整合了一个稳定可靠的实现,包含了S盒、轮密钥生成和加解密轮函数。
// SM4Core.ts - SM4算法核心逻辑
export class SM4Core {
// SM4算法常量定义:S盒与固定参数FK、CK
private static readonly SBOX: number[] = [ /* ... 完整的256位S盒数据 ... */ ];
private static readonly FK: number[] = [0xa3b1bac6, 0x56aa3350, 0x677d9197, 0xb27022dc];
private static readonly CK: number[] = [ /* ... 32个固定参数 ... */ ];
private rk: number[] = new Array(32); // 轮密钥数组
constructor(key: Uint8Array) {
if (key.length !== 16) {
throw new Error('SM4 key must be 16 bytes (128 bits) long.');
}
this.generateRoundKeys(key);
}
// 轮密钥生成函数
private generateRoundKeys(mk: Uint8Array): void {
let k: number[] = new Array(36);
// 将密钥字节数组转换为4个32位字
for (let i = 0; i < 4; i++) {
k[i] = ((mk[i * 4] & 0xff) << 24) |
((mk[i * 4 + 1] & 0xff) << 16) |
((mk[i * 4 + 2] & 0xff) << 8) |
(mk[i * 4 + 3] & 0xff);
k[i] ^= SM4Core.FK[i]; // 与固定参数FK异或
}
// 迭代生成36个中间字
for (let i = 0; i < 32; i++) {
const x = k[i + 1] ^ k[i + 2] ^ k[i + 3] ^ SM4Core.CK[i];
const t = this.tau(x); // 非线性变换
k[i + 4] = k[i] ^ t;
this.rk[i] = k[i + 4]; // 存储为轮密钥
}
}
// 非线性变换tau,包含S盒替换和线性变换L
private tau(a: number): number {
const b = this.sBox(a); // S盒替换
// 线性变换 L'
return b ^ this.rotl(b, 13) ^ this.rotl(b, 23);
}
private sBox(a: number): number {
const b0 = (a >> 24) & 0xff;
const b1 = (a >> 16) & 0xff;
const b2 = (a >> 8) & 0xff;
const b3 = a & 0xff;
return (SM4Core.SBOX[b0] << 24) |
(SM4Core.SBOX[b1] << 16) |
(SM4Core.SBOX[b2] << 8) |
SM4Core.SBOX[b3];
}
private rotl(x: number, n: number): number {
return (x << n) | (x >>> (32 - n));
}
// 单次加/解密轮函数 (加解密共用,轮密钥顺序相反)
private roundFunction(x: number[]): number {
const b = this.sBox(x[1] ^ x[2] ^ x[3] ^ this.rk[0]); // 注意,这里简化了演示,实际需要循环
return x[0] ^ b ^ this.rotl(b, 2) ^ this.rotl(b, 10) ^ this.rotl(b, 18) ^ this.rotl(b, 24);
}
// 加密一个16字节的块
public encryptBlock(input: Uint8Array): Uint8Array {
// ... 具体的32轮加密流程,将输入块转换为4个字,进行32轮迭代 ...
let x: number[] = new Array(4);
for (let i = 0; i < 4; i++) {
x[i] = ((input[i * 4] & 0xff) << 24) |
((input[i * 4 + 1] & 0xff) << 16) |
((input[i * 4 + 2] & 0xff) << 8) |
(input[i * 4 + 3] & 0xff);
}
// 32轮迭代,每轮使用 this.rk[i]
for (let i = 0; i < 32; i++) {
const tmp = this.roundFunction(x);
x.shift(); // 移除x[0]
x.push(tmp); // 将结果加入末尾
}
// 最后反序输出
const output = new Uint8Array(16);
// 将x[3], x[2], x[1], x[0]按顺序转换为字节写入output
// ... 转换代码 ...
return output;
}
// 解密一个16字节的块 (与加密结构相同,仅轮密钥使用顺序相反)
public decryptBlock(input: Uint8Array): Uint8Array {
// 临时反转轮密钥顺序
const originalRk = this.rk.slice();
this.rk.reverse(); // 解密使用逆序轮密钥
const output = this.encryptBlock(input); // 重用加密结构
this.rk = originalRk; // 恢复
return output;
}
}
注意 :以上是SM4核心算法的骨架代码,为了清晰起见,省略了完整的S盒数据、CK数组以及32轮迭代的完整展开。在实际项目中,你需要补全这些常量数据。一个完整的、可运行的SM4实现代码量较大,但结构是清晰的:初始化、轮密钥生成、加密/解密块。
3.2 PKCS#7填充与去填充工具
接下来,我们实现填充工具。这是一个独立的、纯数据处理的工具类。
// PaddingUtil.ts - PKCS#7填充工具
export class PKCS7Padding {
/**
* 对数据进行PKCS#7填充
* @param data 原始数据
* @param blockSize 块大小(对于SM4是16)
* @returns 填充后的数据
*/
static pad(data: Uint8Array, blockSize: number = 16): Uint8Array {
const paddingLength = blockSize - (data.length % blockSize);
const paddedData = new Uint8Array(data.length + paddingLength);
paddedData.set(data); // 拷贝原始数据
// 填充字节的值等于填充长度
for (let i = data.length; i < paddedData.length; i++) {
paddedData[i] = paddingLength;
}
return paddedData;
}
/**
* 去除PKCS#7填充
* @param paddedData 填充后的数据
* @param blockSize 块大小
* @returns 去除填充后的原始数据
* @throws 如果填充格式错误则抛出异常
*/
static unpad(paddedData: Uint8Array, blockSize: number = 16): Uint8Array {
if (paddedData.length === 0 || paddedData.length % blockSize !== 0) {
throw new Error('Invalid padded data length.');
}
const paddingLength = paddedData[paddedData.length - 1];
if (paddingLength <= 0 || paddingLength > blockSize) {
throw new Error('Invalid PKCS#7 padding.');
}
// 验证填充字节的值是否一致
for (let i = paddedData.length - paddingLength; i < paddedData.length; i++) {
if (paddedData[i] !== paddingLength) {
throw new Error('Invalid PKCS#7 padding bytes.');
}
}
// 返回去除填充部分的数据
return paddedData.slice(0, paddedData.length - paddingLength);
}
}
这个工具类非常关键,且容易出错。务必在 unpad 方法中做好完整性校验,防止恶意构造的填充数据导致程序异常或安全漏洞。
3.3 CBC模式工具类封装
现在,我们将SM4核心算法、填充工具和CBC模式逻辑组合起来,形成一个完整的、易于使用的CBC加解密工具类。
// SM4_CBC_Util.ts - 完整的CBC模式工具类
import { SM4Core } from './SM4Core';
import { PKCS7Padding } from './PaddingUtil';
export class SM4_CBC_Util {
private sm4: SM4Core;
/**
* 构造函数
* @param key 16字节的密钥
*/
constructor(key: Uint8Array) {
this.sm4 = new SM4Core(key);
}
/**
* CBC模式加密(同步)
* @param plainData 明文数据
* @param iv 16字节初始化向量
* @returns 密文数据
*/
encryptSync(plainData: Uint8Array, iv: Uint8Array): Uint8Array {
if (iv.length !== 16) {
throw new Error('IV must be 16 bytes long for SM4 CBC mode.');
}
// 1. 对明文进行PKCS#7填充
const paddedData = PKCS7Padding.pad(plainData);
// 2. 初始化前一个密文块(第一个块使用IV)
let previousBlock = new Uint8Array(iv);
// 3. 计算需要加密的块数
const blockCount = paddedData.length / 16;
const cipherData = new Uint8Array(paddedData.length);
// 4. 循环处理每个数据块
for (let i = 0; i < blockCount; i++) {
const start = i * 16;
const plainBlock = paddedData.slice(start, start + 16);
// CBC核心步骤:与前一个密文块(或IV)异或
const blockToEncrypt = new Uint8Array(16);
for (let j = 0; j < 16; j++) {
blockToEncrypt[j] = plainBlock[j] ^ previousBlock[j];
}
// 使用SM4加密异或后的块
const encryptedBlock = this.sm4.encryptBlock(blockToEncrypt);
// 存储密文块,并作为下一个块的“前一个密文块”
cipherData.set(encryptedBlock, start);
previousBlock = encryptedBlock;
}
return cipherData;
}
/**
* CBC模式解密(同步)
* @param cipherData 密文数据
* @param iv 16字节初始化向量(必须与加密时相同)
* @returns 解密并去除填充后的明文数据
*/
decryptSync(cipherData: Uint8Array, iv: Uint8Array): Uint8Array {
if (iv.length !== 16 || cipherData.length % 16 !== 0) {
throw new Error('Invalid IV or cipher data length for SM4 CBC decryption.');
}
let previousBlock = new Uint8Array(iv);
const blockCount = cipherData.length / 16;
const paddedPlainData = new Uint8Array(cipherData.length);
for (let i = 0; i < blockCount; i++) {
const start = i * 16;
const cipherBlock = cipherData.slice(start, start + 16);
// 先解密当前密文块
const decryptedBlock = this.sm4.decryptBlock(cipherBlock);
// CBC解密核心步骤:将解密后的块与前一个密文块(或IV)异或,得到原始明文块
const plainBlock = new Uint8Array(16);
for (let j = 0; j < 16; j++) {
plainBlock[j] = decryptedBlock[j] ^ previousBlock[j];
}
paddedPlainData.set(plainBlock, start);
// 更新“前一个密文块”为当前密文块,用于下一个块的解密
previousBlock = cipherBlock;
}
// 去除PKCS#7填充,返回原始明文
return PKCS7Padding.unpad(paddedPlainData);
}
}
这个工具类已经具备了完整的同步加解密功能。注意解密流程中, previousBlock 在解密阶段指向的是前一个 密文块 ,而不是前一个解密后的明文块,这是CBC模式解密的关键。
4. 异步实现与性能对比
对于移动应用,异步操作是保证流畅体验的基石。HarmonyOS ArkTS提供了 TaskPool API,可以方便地将耗时任务放到后台线程执行。
4.1 基于TaskPool的异步封装
我们将上面的同步方法,用 TaskPool 包装成异步版本。
// SM4_CBC_Async.ts - 异步加解密封装
import { taskpool } from '@kit.TaskPoolKit';
import { SM4_CBC_Util } from './SM4_CBC_Util';
export class SM4_CBC_Async {
private key: Uint8Array;
constructor(key: Uint8Array) {
this.key = key;
}
/**
* 异步加密
* @param plainData 明文数据
* @param iv 初始化向量
* @returns Promise,解析为密文数据
*/
async encryptAsync(plainData: Uint8Array, iv: Uint8Array): Promise<Uint8Array> {
// 使用TaskPool执行耗时任务
return await taskpool.execute(async (): Promise<Uint8Array> => {
const util = new SM4_CBC_Util(this.key);
return util.encryptSync(plainData, iv);
}, plainData, iv); // 将数据作为参数传递。注意:TaskPool参数需要可序列化。
}
/**
* 异步解密
* @param cipherData 密文数据
* @param iv 初始化向量
* @returns Promise,解析为明文数据
*/
async decryptAsync(cipherData: Uint8Array, iv: Uint8Array): Promise<Uint8Array> {
return await taskpool.execute(async (): Promise<Uint8Array> => {
const util = new SM4_CBC_Util(this.key);
return util.decryptSync(cipherData, iv);
}, cipherData, iv);
}
}
重要提示 :
taskpool.execute要求传入的函数和参数必须是可序列化的(即能被PostMessage传递)。Uint8Array是可序列化的。但是,如果你传递的是一个非常庞大的数组(例如几十MB的图片数据),可能会遇到序列化性能瓶颈或内存问题。对于超大文件,建议采用分块处理的方式。
4.2 同步与异步代码使用对比
让我们通过一个具体的页面示例,来看看如何使用这两种方式,并感受它们的区别。
// Index.ets - 示例页面
import { SM4_CBC_Util } from '../utils/SM4_CBC_Util';
import { SM4_CBC_Async } from '../utils/SM4_CBC_Async';
@Entry
@Component
struct Index {
private key: Uint8Array = new Uint8Array([0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef, 0xfe, 0xdc, 0xba, 0x98, 0x76, 0x54, 0x32, 0x10]);
private iv: Uint8Array = new Uint8Array(16); // 全零IV,仅示例,生产环境应用随机IV
private plainText: string = 'Hello HarmonyOS SM4 CBC! 你好,世界!';
build() {
Column({ space: 20 }) {
Button('同步加密解密')
.onClick(() => {
this.syncTest();
})
Button('异步加密解密')
.onClick(() => {
this.asyncTest();
})
Text('结果将打印在Log中')
}
.width('100%')
.height('100%')
.padding(20)
.justifyContent(FlexAlign.Center)
}
// 同步方式测试
private syncTest(): void {
console.log('--- 开始同步测试 ---');
const util = new SM4_CBC_Util(this.key);
const plainData = new TextEncoder().encode(this.plainText);
const startTime = new Date().getTime();
const cipherData = util.encryptSync(plainData, this.iv);
const midTime = new Date().getTime();
const decryptedData = util.decryptSync(cipherData, this.iv);
const endTime = new Date().getTime();
const decryptedText = new TextDecoder().decode(decryptedData);
console.log(`同步加密耗时: ${midTime - startTime}ms`);
console.log(`同步解密耗时: ${endTime - midTime}ms`);
console.log(`解密结果是否正确: ${decryptedText === this.plainText}`);
console.log(`密文(Hex): ${Array.from(cipherData).map(b => b.toString(16).padStart(2, '0')).join('')}`);
}
// 异步方式测试
private async asyncTest(): Promise<void> {
console.log('--- 开始异步测试 ---');
const asyncUtil = new SM4_CBC_Async(this.key);
const plainData = new TextEncoder().encode(this.plainText);
const startTime = new Date().getTime();
try {
// UI线程不会被阻塞,可以继续响应用户操作
const cipherData = await asyncUtil.encryptAsync(plainData, this.iv);
const midTime = new Date().getTime();
const decryptedData = await asyncUtil.decryptAsync(cipherData, this.iv);
const endTime = new Date().getTime();
const decryptedText = new TextDecoder().decode(decryptedData);
console.log(`异步加密耗时: ${midTime - startTime}ms`);
console.log(`异步解密耗时: ${endTime - midTime}ms`);
console.log(`解密结果是否正确: ${decryptedText === this.plainText}`);
// 注意:异步总耗时可能包含线程调度开销,但UI是流畅的
} catch (error) {
console.error('异步加解密失败:', error);
}
}
}
对比分析:
- 同步调用 :
syncTest方法中,从加密开始到解密结束,UI主线程会被完全占用。如果处理的数据很大,界面会“卡住”,直到计算完成。从日志的耗时可以看出,计算是连续进行的。 - 异步调用 :
asyncTest方法中,点击按钮后,加解密任务被丢给TaskPool。await会暂停当前函数的执行,但 不会阻塞UI线程 。按钮可以立刻弹起,页面可以滚动,用户体验流畅。日志中的耗时反映了后台线程的实际计算时间。
选择建议:
- 使用同步方式 :当加密数据非常小(如一个ID、一个令牌),且操作在页面初始化或非交互流程中时。
- 使用异步方式 :当加密数据量较大(如超过1KB)、在UI事件回调中触发、或需要加密文件时。这是移动应用开发的 最佳实践 。
5. 实战技巧与避坑指南
在实际项目中使用这套代码,有几个必须注意的细节和容易踩的坑。
5.1 密钥与IV的安全管理
密钥(Key) :
- 绝对不要硬编码在代码中 。这是最低级也是最危险的安全错误。
- 对于客户端加密,密钥可以来自:
- 用户输入的口令(Password) :通过PBKDF2、Scrypt等密钥派生函数(KDF)生成固定长度的密钥。这需要你额外实现KDF。
- 与服务器协商 :在登录后,由服务端下发一个本次会话的临时加密密钥。
- 安全存储 :生成的密钥应使用HarmonyOS的
@kit.CipherKit或安全沙箱进行加密存储,而不是直接放在Preferences或文件中。
初始化向量(IV) :
- 必须随机生成 ,每次加密都应使用新的随机IV。可以使用
@kit.SecurityKit中的cryptoFramework来生成密码学安全的随机数。 - IV不需要保密 ,但必须唯一且不可预测。通常将IV和密文一起存储或传输。解密时,使用相同的IV即可。
- 绝对不要重复使用相同的Key和IV组合 来加密不同的数据,这会严重削弱CBC模式的安全性。
import { cryptoFramework } from '@kit.SecurityKit';
async function generateRandomIV(): Promise<Uint8Array> {
const rand = cryptoFramework.createRandom();
const iv = await rand.generateRandomBytes(16); // 生成16字节随机IV
return iv;
}
5.2 数据格式与兼容性处理
在与其他系统(尤其是服务端)交互时,数据格式必须约定一致。
- 字节与字符串转换 :网络传输和存储通常是基于文本的(如JSON)。你需要将
Uint8Array类型的密文转换为Base64或Hex字符串。// 加密后转换为Base64 const cipherData: Uint8Array = util.encryptSync(plainData, iv); const cipherBase64 = buffer.from(cipherData).toString('base64'); // 使用buffer模块 // 解密前从Base64转换回来 const cipherDataFromBase64 = buffer.from(cipherBase64, 'base64'); const decryptedData = util.decryptSync(cipherDataFromBase64, iv); - 服务端对接 :确保服务端使用的SM4库也支持PKCS#7填充和CBC模式。有些库的默认填充方式可能是不同的(如ZeroPadding),不一致会导致解密失败或得到乱码。
- IV的传递 :将IV以Hex或Base64格式,与密文一起传递给服务端。常见的做法是
IV + 密文拼接后传输,或者将两者作为JSON对象的不同字段。
5.3 性能优化与大数据处理
对于非常大的数据(如图片、视频、数据库文件),直接调用上述方法可能会内存溢出或造成长时间卡顿。
- 分块处理 :实现一个流式处理的接口。将大文件分割成多个适当大小的块(如64KB),逐块进行CBC加密。注意,CBC模式是链式的, 加密下一块时需要上一块的密文作为反馈 ,因此分块处理时需要小心维护这个链式状态。通常,对于文件加密,更推荐使用支持流式加密的库或模式(如CTR模式),如果必须用CBC,则需要自己管理块之间的IV传递。
- 使用WebAssembly :如果性能要求极高,可以考虑将核心的SM4算法用C/C++编写,并编译为WebAssembly模块,在ArkTS中调用。这能获得接近原生的性能,但复杂度也大大增加。
5.4 常见问题排查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 解密后得到乱码 | 1. 密钥不一致。 2. IV不一致或错误。 3. 数据填充方式不一致。 |
1. 核对双方密钥的字节序列是否完全一致。 2. 确认加密端和解密端使用的IV是同一个。检查IV的生成、存储和传输过程。 3. 确认双方都使用PKCS#7填充。可以尝试在解密后打印去除填充前的数据,看最后一个字节的值是否合理。 |
| 解密时抛出“Invalid padding”异常 | 1. 密文在传输或存储过程中被损坏。 2. 密钥或IV错误,导致解密出的数据最后一个字节不是有效的填充值。 |
1. 检查密文数据的完整性。对比加密后的Base64字符串和解密时收到的字符串是否完全相同。 2. 优先检查密钥和IV的正确性。 |
| 异步加解密返回失败或崩溃 | 1. 传递给 taskpool.execute 的数据不可序列化或过大。 2. 后台任务中抛出了未捕获的异常。 |
1. 确保参数是简单的、可序列化的数据类型。对于超大对象,考虑分块或使用 SharedArrayBuffer (需谨慎)。 2. 在传递给TaskPool的函数内部使用try-catch包裹核心逻辑,并将错误信息返回。 |
| 加密结果与第三方工具/服务端不一致 | 1. 字符编码问题。明文“你好”在加密前,UTF-8和GBK编码的字节数组不同。 2. 输出格式不同。对方显示的是Hex大写,你的是Hex小写或Base64。 3. SM4实现细节差异(极少见)。 |
1. 统一编码 :在加密前,明确使用UTF-8编码将字符串转为字节数组 ( new TextEncoder().encode() )。 2. 统一输出 :约定都使用Hex小写(无分隔符)或Base64进行比较。 3. 使用标准的测试向量验证自己的SM4核心算法实现是否正确。 |
最后,再分享一个我踩过的坑:在测试与Java服务端对接时,发现解密失败。折腾了半天才发现,Java端用的SM4库,默认的IV参数是一个 AlgorithmParameterSpec 对象,而我最初传递IV字节数组的方式不对。后来统一为“将IV字节数组包装成 IvParameterSpec 对象”后,问题才解决。所以,跨平台对接时,不仅要看算法和模式,还要看具体API对参数的要求。
更多推荐


所有评论(0)