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模式就是为了解决这个问题而生的。它的核心思想是“链式”加密:

  1. 初始化向量(IV) :首先需要一个随机生成的、长度也为16字节的IV。这个IV不需要保密,但必须不可预测,通常每次加密都随机生成。
  2. 异或操作 :在加密第一个明文块之前,先将其与IV进行按位异或(XOR)操作。
  3. 加密 :将异或后的结果送入SM4算法进行加密,得到第一个密文块。
  4. 链式反馈 :在加密第二个明文块时,不再使用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 数据格式与兼容性处理

在与其他系统(尤其是服务端)交互时,数据格式必须约定一致。

  1. 字节与字符串转换 :网络传输和存储通常是基于文本的(如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);
    
  2. 服务端对接 :确保服务端使用的SM4库也支持PKCS#7填充和CBC模式。有些库的默认填充方式可能是不同的(如ZeroPadding),不一致会导致解密失败或得到乱码。
  3. 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对参数的要求。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐