一、背景说明

HarmonyOS 开发者官方今日持续直播讲解应用安全主题,数据落盘与传输的机密性保护是其中绕不开的一环。本文不展开安全体系,聚焦一个具体技术点:使用 Crypto Architecture Kit(@kit.CryptoArchitectureKit)的 cryptoFramework,在鸿蒙应用内实现 AES-128-GCM 加解密的完整闭环——密钥生成、GCM 参数构造、加密、认证标签提取、解密与篡改校验,全部代码可直接编译运行。

二、技术原理

cryptoFramework 是鸿蒙统一的加解密算法框架,屏蔽底层硬件差异(支持软算法与硬件加速),核心概念如下:

  • SymKeyGenerator:对称密钥生成器,createSymKeyGenerator('AES128') 指定算法与长度,generateSymKey() 随机生成,convertKey() 从已有字节恢复。
  • Cipher:加解密实例,createCipher('AES128|GCM') 用字符串声明算法/分组模式(分段场景可加 |PKCS7 填充)。
  • GcmParamsSpec:GCM 模式(AEAD 认证加密)的三要素——iv(12 字节随机初始化向量)、aad(附加认证数据,可不参与加密但参与完整性校验)、authTag(16 字节认证标签)。
  • CryptoMode:ENCRYPT_MODE / DECRYPT_MODE,通过 cipher.init() 指定。

GCM 模式的关键机制:加密时 update 输出密文、doFinal(null) 输出 16 字节 authTag;解密时把 authTag 填回 GcmParamsSpec 传入 init,若密文或 AAD 被篡改,doFinal 校验失败抛出异常(错误码 17630001)。数据流:

createSymKeyGenerator → generateSymKey → createCipher('AES128|GCM') → init(模式, key, GcmParamsSpec) → update(明文) → doFinal(null)=authTag → [密文+tag+iv 一起存储/传输] → 解密端 init 时回填 tag → update(密文) → doFinal 得到明文。

三、实现步骤

工程为 Stage 模型(API 12+),加解密属于应用内纯计算,无需任何权限配置,module.json5 保持默认。

1. 加解密工具类 CryptoUtil.ets
import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { util } from '@kit.ArkTS';

export interface GcmCipherResult {
  cipherText: Uint8Array; // 密文(不含 tag)
  authTag: Uint8Array;    // 16 字节认证标签
  iv: Uint8Array;         // 本次加密使用的 iv,解密时需一并保存/传输
}

export class CryptoUtil {

  // 随机生成 AES-128 对称密钥
  static async generateAesKey(): Promise<cryptoFramework.SymKey> {
    const generator = cryptoFramework.createSymKeyGenerator('AES128');
    return await generator.generateSymKey();
  }

  // 从已有字节恢复密钥(如从 Asset Store / 服务端下发)
  static async convertAesKey(raw: Uint8Array): Promise<cryptoFramework.SymKey> {
    const generator = cryptoFramework.createSymKeyGenerator('AES128');
    return await generator.convertKey({ data: raw });
  }

  // 构造 GCM 三要素:iv 随机 12 字节,aad 附加认证数据,tag 先占位 16 字节
  private static genGcmParams(aad: Uint8Array): cryptoFramework.GcmParamsSpec {
    const ivBlob = cryptoFramework.createRandom().generateRandomSync(12);
    const tagBlob: cryptoFramework.DataBlob = { data: new Uint8Array(16) };
    return {
      iv: ivBlob,
      aad: { data: aad },
      authTag: tagBlob,
      algName: 'GcmParamsSpec'
    };
  }

  // 加密:返回密文 + tag + iv
  static async encrypt(symKey: cryptoFramework.SymKey,
    plainText: string, aad: Uint8Array): Promise<GcmCipherResult> {
    const gcmParams = CryptoUtil.genGcmParams(aad);
    const cipher = cryptoFramework.createCipher('AES128|GCM');
    await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, gcmParams);

    const plainBlob: cryptoFramework.DataBlob = {
      data: new Uint8Array(util.TextEncoder.create('utf-8').encodeInto(plainText).buffer)
    };
    const encryptUpdate = await cipher.update(plainBlob);
    // doFinal(null) 在 GCM 模式下返回 16 字节 authTag
    gcmParams.authTag = (await cipher.doFinal(null)) as cryptoFramework.DataBlob;

    return {
      cipherText: encryptUpdate.data,
      authTag: gcmParams.authTag.data,
      iv: gcmParams.iv.data
    };
  }

  // 解密:密文、tag、iv 齐全才能还原;tag 校验失败会抛异常
  static async decrypt(symKey: cryptoFramework.SymKey,
    result: GcmCipherResult, aad: Uint8Array): Promise<string> {
    const gcmParams: cryptoFramework.GcmParamsSpec = {
      iv: { data: result.iv },
      aad: { data: aad },
      authTag: { data: result.authTag },
      algName: 'GcmParamsSpec'
    };
    const cipher = cryptoFramework.createCipher('AES128|GCM');
    await cipher.init(cryptoFramework.CryptoMode.DECRYPT_MODE, symKey, gcmParams);
    const decryptUpdate = await cipher.update({ data: result.cipherText });
    await cipher.doFinal(null); // 校验 tag,篡改时此处抛 17630001
    return util.TextDecoder.create('utf-8').decodeToString(decryptUpdate.data);
  }
}
2. UI 调用示例 Index.ets
import { BusinessError } from '@kit.BasicServicesKit';
import { CryptoUtil, GcmCipherResult } from '../model/CryptoUtil';

@Entry
@Component
struct Index {
  @State originText: string = '鸿蒙 AES-GCM 加密测试数据 2026';
  @State cipherResult: GcmCipherResult | null = null;
  @State decryptedText: string = '';
  private symKey: cryptoFramework.SymKey | null = null;

  // 一次性生成密钥,真实场景应持久化到 Asset Store Kit
  private async ensureKey(): Promise<cryptoFramework.SymKey> {
    if (!this.symKey) {
      this.symKey = await CryptoUtil.generateAesKey();
    }
    return this.symKey;
  }

  doEncrypt(): void {
    this.ensureKey()
      .then((key) => CryptoUtil.encrypt(key, this.originText,
        new Uint8Array(util.TextEncoder.create('utf-8').encodeInto('app-header').buffer)))
      .then((r: GcmCipherResult) => {
        this.cipherResult = r;
        console.info(`encrypt ok, cipher len=${r.cipherText.length}, tag len=${r.authTag.length}`);
      })
      .catch((err: BusinessError) => console.error(`encrypt failed: ${err.code}`));
  }

  doDecrypt(): void {
    if (!this.cipherResult) { return; }
    this.ensureKey()
      .then((key) => CryptoUtil.decrypt(key, this.cipherResult!,
        new Uint8Array(util.TextEncoder.create('utf-8').encodeInto('app-header').buffer)))
      .then((plain: string) => { this.decryptedText = plain; })
      .catch((err: BusinessError) => {
        if (err.code === 17630001) {
          this.decryptedText = '解密失败:数据已被篡改';
        } else {
          this.decryptedText = `解密失败 code=${err.code}`;
        }
      });
  }

  build() {
    Column({ space: 12 }) {
      TextInput({ text: this.originText })
        .onChange((v: string) => { this.originText = v; })
        .width('90%')
      Row({ space: 12 }) {
        Button('加密').onClick(() => { this.doEncrypt(); })
        Button('解密').onClick(() => { this.doDecrypt(); })
      }
      Text(this.cipherResult
        ? `密文(${this.cipherResult.cipherText.length}B): ${Array.from(this.cipherResult.cipherText.slice(0, 8)).toString()}...`
        : '尚未加密')
        .fontSize(13)
      Text(`解密结果:${this.decryptedText || '尚未解密'}`)
        .fontSize(15)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}
四、应用场景示例

大文件(如聊天图片、离线包)需要分段加密时,多次 update 拼接即可,避免一次性占用大内存:

static async encryptBySegment(symKey: cryptoFramework.SymKey,
  chunks: Uint8Array[], aad: Uint8Array): Promise<{ data: Uint8Array; tag: Uint8Array }> {
  const gcmParams = CryptoUtil.genGcmParams(aad);
  const cipher = cryptoFramework.createCipher('AES128|GCM|PKCS7');
  await cipher.init(cryptoFramework.CryptoMode.ENCRYPT_MODE, symKey, gcmParams);
  const out: number[] = [];
  for (const chunk of chunks) {
    const r = await cipher.update({ data: chunk });
    if (r) { out.push(...r.data); } // update 结果可能为 null,需判空后拼接
  }
  const tag = await cipher.doFinal(null);
  return { data: new Uint8Array(out), tag: tag.data };
}
五、技术要点
1. authTag 的生命周期

GCM 加密输出 = 密文 + 16 字节 authTag,存储/传输时必须把 tag、iv 与密文一起保存;解密端三者缺一不可,iv 建议每次加密都重新随机生成(12 字节),绝不能复用同一 (key, iv) 组合加密两条消息。

2. AAD 参与认证但不加密

aad 中的数据不会出现在密文里,但一旦解密时传入的 AAD 与加密时不一致,doFinal 同样校验失败——适合把协议头、版本号等"需要保护但不保密"的字段放进去。

3. 错误处理
try {
  await CryptoUtil.decrypt(key, result, aad);
} catch (err) {
  const e = err as BusinessError;
  if (e.code === 17630001) {
    console.error('GCM tag 校验失败:密文/tag/aad 被篡改或与加密参数不一致');
  } else if (e.code === 401) {
    console.error('参数错误:检查算法字符串与 GcmParamsSpec.algName');
  } else if (e.code === 801) {
    console.error('设备不支持该算法规格,需降级方案');
  }
}
4. 使用限制
  • 算法字符串规格严格:AES128|GCM、AES256|GCM 合法,AES-GCM 或 aes128|gcm 会报 401;
  • update 结果与 doFinal 结果都可能为 null,访问 .data 前必须判空;
  • 非对称加密(RSA)单次加解密数据量受密钥长度限制(如 RSA2048 单次最多 245 字节),大数据量请走"RSA 加密 AES 密钥 + AES 加密数据"的混合方案。
六、扩展建议
  1. 密钥安全存储:用 Asset Store Kit 把 AES 原始字节存入系统级资产库(ASSET_TYPE_BYTES),运行时按需读取,避免密钥明文落盘;
  2. 硬件级保护:接入 HUKS(Universal Keystore Kit)让密钥生成与加解密全部在 TEE/安全环境内完成,应用侧只拿句柄;
  3. 国密算法:同一套 API 换算法规格即可切换国密:
// SM4-GCM:与 AES 用法完全一致,仅算法字符串不同
const cipher = cryptoFramework.createCipher('SM4|GCM|PKCS7');
结语

本文用 cryptoFramework 完成了鸿蒙应用内 AES-128-GCM 从密钥生成、加密、认证标签管理到解密校验的完整实现,核心代码约 160 行,均可直接编译运行。掌握 GCM 的 tag/iv/AAD 三要素后,应用内的本地敏感数据落盘、接口报文加密都有了标准且经过认证加密保护的实现方案。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐