【鸿蒙】HarmonyOS 安全:加密算法与 HUKS 密钥管理

HarmonyOS 安全:加密算法与 HUKS 密钥管理

> 一句话收益:掌握 HarmonyOS HUKS(通用密钥库)的完整密钥生命周期管理与主流加密算法使用,避免密钥泄露与加密滥用陷阱。

> 适用版本:HarmonyOS NEXT / API 12+

> 阅读时长:约 18 分钟


1. 从一个真实 Bug 切入

某金融类鸿蒙应用在上线前安全审计时,被发现了一个严重漏洞:开发者使用 cryptoFramework 生成了一个 AES 密钥,然后将密钥的原始字节序列化存入 Preferences 中,下次启动再读取出来解密用户数据。

审计报告写道:"密钥以明文存储于应用沙箱,攻击者通过备份提取或 root 设备可直接获取密钥,从而解密所有用户敏感数据。"

这个问题本质是:密钥从未离开过应用层 。而 HarmonyOS 提供的 HUKS(Hardware Universal KeyStore)正是为了让密钥永远不出安全硬件而设计的。

本文将系统讲解 HUKS 架构、密钥全生命周期、主流加密算法实战,以及最常踩的几类安全坑。


2. HUKS 架构全景

2.1 HUKS 是什么

HUKS(Hardware Universal KeyStore)是 HarmonyOS 提供的系统级密钥管理服务,核心设计原则:

  • 密钥不出 TEE:密钥材料在可信执行环境(TEE)或安全芯片中生成和存储,应用层只持有密钥别名(alias)

  • 基于属性的访问控制:密钥生成时绑定用途(加密/签名/HMAC)、算法、密钥长度等属性,运行时强制校验

  • 用户认证绑定:支持将密钥访问与指纹/PIN 认证绑定,未认证时无法使用

2.2 架构层次

复制代码
┌─────────────────────────────────────┐

│            应用层 ArkTS              │


│   huks.generateKeyItem()            │


│   huks.encryptItem()  (别名操作)     │


└────────────────┬────────────────────┘


│ IPC


┌────────────────▼────────────────────┐


│          HUKS Service(系统服务)    │


│   密钥元数据管理 / 属性校验          │


└────────────────┬────────────────────┘


│ 安全通道


┌────────────────▼────────────────────┐


│       HUKS Core(TEE / 安全芯片)    │


│   真实密钥材料存储与密码运算         │


│   密钥材料永不离开此层               │


└─────────────────────────────────────┘

2.3 与 cryptoFramework 的区别

| 维度 | HUKS | cryptoFramework |

|------|------|-----------------|

| 密钥存储 | TEE/安全芯片,不暴露明文 | 应用内存,可导出 |

| 适用场景 | 长期密钥、敏感凭证 | 临时加密、文件加密(配合HUKS使用) |

| 密钥导出 | 默认不可导出 | 可导出字节数组 |

| 用户认证绑定 | 支持 | 不支持 |

| 性能 | 较低(跨TEE调用) | 较高(纯软件) |

正确模式:用 HUKS 管理长期密钥,用 cryptoFramework 做临时数据加解密,两者配合使用


3. HUKS 密钥全生命周期

复制代码
生成密钥                  使用密钥                  删除密钥

generateKeyItem()  ──►  encryptItem()         ──►  deleteKeyItem()


decryptItem()


signItem() / verifyItem()


agreeKeyItem()        ──►  exportKeyItem()(可选)


(仅公钥可导出)

关键 API(均在 @ohos.security.huks 模块):

| API | 说明 |

|-----|------|

| huks.generateKeyItem(keyAlias, options, callback) | 生成密钥,存入 HUKS |

| huks.importKeyItem(keyAlias, options, callback) | 导入外部密钥 |

| huks.exportKeyItem(keyAlias, options, callback) | 导出公钥(非对称密钥) |

| huks.deleteKeyItem(keyAlias, options, callback) | 删除密钥 |

| huks.encryptItem(keyAlias, options, callback) | HUKS 内加密 |

| huks.decryptItem(keyAlias, options, callback) | HUKS 内解密 |

| huks.signItem(keyAlias, options, callback) | 签名 |

| huks.verifyItem(keyAlias, options, callback) | 验签 |

| huks.initSession() / updateSession() / finishSession() | 分段处理大数据 |

| huks.isKeyItemExist(keyAlias, options, callback) | 检查密钥是否存在 |


4. 代码示例

4.1 AES-256-GCM 加解密(正确写法)

复制代码
import huks from '@ohos.security.huks';

import { BusinessError } from '@ohos.base';



const KEY_ALIAS = 'myAppAesKey_v1';



// ── 辅助函数:构建属性集 ──────────────────────────────────────────


function buildAesKeyGenOptions(): huks.HuksOptions {


return {


properties: [


{ tag: huks.HuksTag.HUKS_TAG_ALGORITHM,    value: huks.HuksKeyAlg.HUKS_ALG_AES },


{ tag: huks.HuksTag.HUKS_TAG_KEY_SIZE,     value: huks.HuksKeySize.HUKS_AES_KEY_SIZE_256 },


{ tag: huks.HuksTag.HUKS_TAG_PURPOSE,


// 同时声明加密和解密用途


value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT |


huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT },


]


};


}



function buildAesEncryptOptions(iv: Uint8Array): huks.HuksOptions {


return {


properties: [


{ tag: huks.HuksTag.HUKS_TAG_ALGORITHM,      value: huks.HuksKeyAlg.HUKS_ALG_AES },


{ tag: huks.HuksTag.HUKS_TAG_PURPOSE,        value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT },


{ tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE,     value: huks.HuksCipherMode.HUKS_MODE_GCM },


{ tag: huks.HuksTag.HUKS_TAG_PADDING,        value: huks.HuksKeyPadding.HUKS_PADDING_NONE },


{ tag: huks.HuksTag.HUKS_TAG_NONCE,          value: iv },   // GCM 用 Nonce,12 字节


{ tag: huks.HuksTag.HUKS_TAG_ASSOCIATED_DATA, value: new Uint8Array([0x00]) }, // AAD


]


};


}



// ── Step 1:生成密钥(若已存在则跳过)────────────────────────────


async function ensureKeyExists(): Promise
   
     {
   


const exist = await huks.isKeyItemExist(KEY_ALIAS, { properties: [] });


if (exist.valueOf()) return;



await huks.generateKeyItem(KEY_ALIAS, buildAesKeyGenOptions());


console.info('HUKS key generated');


}



// ── Step 2:加密 ──────────────────────────────────────────────────


async function encrypt(plaintext: Uint8Array): Promise<{ ciphertext: Uint8Array; iv: Uint8Array }> {


await ensureKeyExists();



// 每次加密生成随机 IV(GCM nonce = 12 字节)


const iv = new Uint8Array(12);


// 使用系统随机数填充 iv(实际项目应使用 crypto.getRandomValues)


for (let i = 0; i < 12; i++) iv[i] = Math.floor(Math.random() * 256);



const options = buildAesEncryptOptions(iv);


options.inData = plaintext;  // 待加密数据



const result = await huks.encryptItem(KEY_ALIAS, options);


return { ciphertext: result.outData!, iv };


}



// ── Step 3:解密 ──────────────────────────────────────────────────


async function decrypt(ciphertext: Uint8Array, iv: Uint8Array): Promise
   
     {
   


const options: huks.HuksOptions = {


properties: [


{ tag: huks.HuksTag.HUKS_TAG_ALGORITHM,      value: huks.HuksKeyAlg.HUKS_ALG_AES },


{ tag: huks.HuksTag.HUKS_TAG_PURPOSE,        value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT },


{ tag: huks.HuksTag.HUKS_TAG_BLOCK_MODE,     value: huks.HuksCipherMode.HUKS_MODE_GCM },


{ tag: huks.HuksTag.HUKS_TAG_PADDING,        value: huks.HuksKeyPadding.HUKS_PADDING_NONE },


{ tag: huks.HuksTag.HUKS_TAG_NONCE,          value: iv },


{ tag: huks.HuksTag.HUKS_TAG_ASSOCIATED_DATA, value: new Uint8Array([0x00]) },


],


inData: ciphertext


};


const result = await huks.decryptItem(KEY_ALIAS, options);


return result.outData!;


}

4.2 错误写法 → 问题 → 正确写法

错误写法:密钥明文存储

复制代码
// ❌ 错误:密钥存入 Preferences,完全暴露

import preferences from '@ohos.data.preferences';


import cryptoFramework from '@ohos.security.cryptoFramework';



const keyGen = cryptoFramework.createSymKeyGenerator('AES256');


const key = await keyGen.generateSymKey();


const keyBytes = await key.getEncoded(); // 导出明文字节



const pref = await preferences.getPreferences(ctx, 'config');


await pref.put('secretKey', Array.from(keyBytes.data).toString()); // 明文写入

问题分析:

  • getEncoded() 会将 AES 密钥明文导出到应用内存

  • 序列化后存入沙箱文件,root 设备或备份提取即可拿到密钥

  • 一旦密钥泄露,所有加密数据均可被解密
    正确写法:密钥留在 HUKS

    // ✅ 正确:密钥材料永远在 TEE 中,应用只持有 alias 字符串

    const KEY_ALIAS = 'myAppAesKey_v1'; // 只存这个字符串

    // 首次启动时生成并存入 HUKS,不导出任何字节

    await huks.generateKeyItem(KEY_ALIAS, buildAesKeyGenOptions());

    // 加密时传别名,HUKS 在 TEE 内完成运算,只返回密文

    const { ciphertext, iv } = await encrypt(plaindata);

    // 只需持久化 ciphertext 和 iv,不需要存密钥


5. RSA 签名实战(非对称密钥)

复制代码
const RSA_KEY_ALIAS = 'myRsaSignKey';


// 生成 RSA-2048 密钥对


async function generateRsaKeyPair(): Promise
   
     {
   


const options: huks.HuksOptions = {


properties: [


{ tag: huks.HuksTag.HUKS_TAG_ALGORITHM,  value: huks.HuksKeyAlg.HUKS_ALG_RSA },


{ tag: huks.HuksTag.HUKS_TAG_KEY_SIZE,   value: huks.HuksKeySize.HUKS_RSA_KEY_SIZE_2048 },


{ tag: huks.HuksTag.HUKS_TAG_PURPOSE,


value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_SIGN |


huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_VERIFY },


{ tag: huks.HuksTag.HUKS_TAG_DIGEST,     value: huks.HuksKeyDigest.HUKS_DIGEST_SHA256 },


{ tag: huks.HuksTag.HUKS_TAG_PADDING,    value: huks.HuksKeyPadding.HUKS_PADDING_PSS },


]


};


await huks.generateKeyItem(RSA_KEY_ALIAS, options);


}



// 签名(私钥在 TEE 中,永不暴露)


async function sign(data: Uint8Array): Promise
   
     {
   


const options: huks.HuksOptions = {


properties: [


{ tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_RSA },


{ tag: huks.HuksTag.HUKS_TAG_PURPOSE,   value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_SIGN },


{ tag: huks.HuksTag.HUKS_TAG_DIGEST,    value: huks.HuksKeyDigest.HUKS_DIGEST_SHA256 },


{ tag: huks.HuksTag.HUKS_TAG_PADDING,   value: huks.HuksKeyPadding.HUKS_PADDING_PSS },


],


inData: data


};


const result = await huks.signItem(RSA_KEY_ALIAS, options);


return result.outData!;


}



// 导出公钥(非对称密钥的公钥可以导出,私钥不行)


async function exportPublicKey(): Promise
   
     {
   


const result = await huks.exportKeyItem(RSA_KEY_ALIAS, { properties: [] });


return result.outData!; // X.509 SubjectPublicKeyInfo 格式


}

6. 最佳实践

实践 1:密钥 alias 按功能+版本命名,禁止使用动态字符串

做法:const ALIAS = 'payment_aes_v2',硬编码,不拼接用户 ID。

原因:动态 alias(如 aes_key_${userId})会导致同一设备上为每个用户生成独立密钥,密钥数量不可控,且密钥泄露面扩大。

不这样做:每次用随机 alias 生成密钥,密钥无法复用,且无法正确删除,HUKS 存储会逐渐膨胀。


实践 2:大数据加密用 initSession/updateSession/finishSession 三段式,勿直接 encryptItem

做法:超过 64KB 的数据使用分段 API:

复制代码
// 分段加密大文件

const handle = await huks.initSession(KEY_ALIAS, encryptOptions);


for (const chunk of chunks) {


await huks.updateSession(handle.handle, { inData: chunk, properties: [] });


}


const finalResult = await huks.finishSession(handle.handle, { properties: [] });

原因:encryptItem 内部有数据大小限制(不同设备不同,通常 64KB~128KB),超出会抛 HuksErrcode.HUKS_ERR_CODE_INVALID_ARGUMENT

不这样做:直接传大数组给 encryptItem,低端设备上必然报错,且无法处理流式文件加密场景。


实践 3:GCM 模式每次加密必须使用全新随机 Nonce,绝对禁止复用

做法:每次 encrypt() 调用 crypto.getRandomValues() 生成 12 字节随机 nonce,将 nonce 与密文一起存储(nonce 无需保密)。

原因:GCM 的安全性依赖于 (Key, Nonce) 对的唯一性。同一密钥下 Nonce 复用两次,攻击者可通过 XOR 两段密文恢复明文,GCM 的认证标签也会失效。

不这样做:固定 Nonce(如全零)或用计数器但不持久化计数器,前者直接导致 GCM 安全性崩溃,后者在应用重启后计数器归零导致 Nonce 复用。


实践 4:需要用户认证才能解密的数据,生成密钥时设置 SECURE_SIGN_TYPE

做法:

复制代码
{ tag: huks.HuksTag.HUKS_TAG_USER_AUTH_TYPE,

value: huks.HuksUserAuthType.HUKS_USER_AUTH_TYPE_FINGERPRINT |


huks.HuksUserAuthType.HUKS_USER_AUTH_TYPE_PIN },


{ tag: huks.HuksTag.HUKS_TAG_KEY_AUTH_ACCESS_TYPE,


value: huks.HuksAuthAccessType.HUKS_AUTH_ACCESS_INVALID_CLEAR_PASSWORD },

原因:将密钥访问与用户认证绑定,即使攻击者拿到 alias 字符串,在未通过生物认证时 HUKS 会拒绝解密操作,密钥材料始终安全。

不这样做:不设置用户认证绑定,密钥随时可用,应用被恶意代码注入后攻击者可直接调用解密接口。


实践 5:应用卸载前主动删除 HUKS 密钥

做法:在 AbilityStage.onDestroy() 或卸载前回调中调用 huks.deleteKeyItem(alias, options)

原因:部分设备上 HUKS 密钥与应用沙箱绑定,卸载后密钥自动清除;但部分厂商实现中密钥会残留,导致重装后 isKeyItemExist 返回 true 但密钥属性已损坏,加密操作会报神秘错误。

不这样做:不处理残留密钥,重装后尝试用旧 alias 加密会得到 HUKS_ERR_CODE_KEY_AUTH_FAILED,且无法通过任何方式修复,只能更换 alias。


7. 常见坑点

坑 1:encryptItem 返回的数据包含 GCM Tag,但没有文档明确说明

现象 :解密时总是失败,密文长度比预期多 16 字节。 原因 :AES-GCM 模式下,HUKS encryptItem 返回的 outData 实际上是 密文 + 16字节 GCM认证Tag 的拼接,调用 decryptItem 时也需要传入这个完整的 128 位 tag。 复现 :用 GCM 模式加密一段 32 字节数据,观察 outData.length,会得到 48 而非 32。 解决 :直接将 encryptItem 的完整 outData 传给 decryptItem,不需要手动分离 tag,HUKS 内部会处理。

复制代码
// ✅ 正确:直接传完整密文(包含GCM Tag)

options.inData = encryptResult.outData;  // 不要截断!


const decResult = await huks.decryptItem(KEY_ALIAS, options);

坑 2:HuksOptions 属性顺序影响操作结果(部分设备)

现象 :相同的属性,调换顺序后在某些华为设备上返回 HUKS_ERR_CODE_INVALID_ARGUMENT原因 :HUKS Service 的 HAL 层实现存在厂商差异,部分设备对属性数组的遍历顺序敏感。 复现 :把 HUKS_TAG_PURPOSE 放到数组末尾,在低版本固件上触发。 解决 :按官方文档示例中属性的固定顺序书写,始终将 ALGORITHMPURPOSEKEY_SIZE/BLOCK_MODE/PADDING/DIGEST 的顺序排列,不随意调换。


坑 3:isKeyItemExist 返回 true 但后续操作报 HUKS_ERR_CODE_KEY_NOT_EXIST

现象 :检查密钥存在 → 然后加密 → 却报密钥不存在。 原因 :多线程场景下,另一个协程或其他进程在检查和使用之间删除了密钥(TOCTOU 竞争);或密钥属于不同用户上下文(多用户设备)。 复现 :开两个 Worker 线程同时操作同一个 alias,一个在加密,另一个在删除。 解决 :不依赖 isKeyItemExist 的结果做控制流,改为直接 try/catch 操作,在 catch 中判断错误码:

复制代码
try {

await huks.encryptItem(KEY_ALIAS, options);


} catch (e) {


const err = e as BusinessError;


if (err.code === huks.HuksErrcode.HUKS_ERR_CODE_KEY_NOT_EXIST) {


// 重新生成密钥后重试


await huks.generateKeyItem(KEY_ALIAS, buildAesKeyGenOptions());


}


}

坑 4:用户认证绑定的密钥在锁屏后无法使用

现象 :设置了指纹认证绑定的密钥,在用户锁屏后调用解密返回 HUKS_ERR_CODE_KEY_AUTH_FAILED,即使传了认证 Token。 原因HUKS_AUTH_ACCESS_INVALID_CLEAR_PASSWORD(清除密码时失效)和 HUKS_AUTH_ACCESS_INVALID_NEW_BIO_ENROLL(新增生物特征时失效)是两种不同的访问策略。锁屏后需要重新触发用户认证获取新的 authToken。 复现 :生成绑定指纹的密钥 → 锁屏 → 后台服务尝试解密。 解决 :需要在后台持续解密的场景,不要绑定用户认证;只在用户主动操作的前台场景绑定认证,并在每次操作前通过 userIAM.auth 模块获取新鲜的 authToken。


坑 5:同一 alias 重复调用 generateKeyItem 不报错但密钥被覆盖

现象 :应用重启时再次生成同名密钥,没有报错,但之前加密的数据解密失败。 原因 :HUKS 对已存在 alias 的重复生成操作,在 API 12 下默认 覆盖 旧密钥(不同版本行为可能不同),旧密钥被静默替换,导致之前加密的数据无法解密。 复现 :调用两次 generateKeyItem(KEY_ALIAS, ...) 使用相同 alias,中间不调用 delete。 解决 :必须用 isKeyItemExist 检查,只在不存在时生成:

复制代码
const exist = await huks.isKeyItemExist(KEY_ALIAS, { properties: [] });

if (!exist.valueOf()) {


await huks.generateKeyItem(KEY_ALIAS, genOptions);


}

8. 总结

  1. 密钥永远不出 TEE:长期密钥必须通过 HUKS 管理,禁止用 cryptoFramework 生成后序列化存储

  2. GCM 模式 Nonce 必须随机且唯一:每次加密生成新随机 Nonce,与密文一起存储

  3. 分段 API 处理大数据:超 64KB 数据必须用 init/update/finish 三段式

  4. 密钥生成加幂等保护 :生成前检查 isKeyItemExist,避免覆盖旧密钥导致已加密数据丢失

  5. 应用卸载清理密钥 :主动调用 deleteKeyItem 避免残留密钥在重装后引发诡异错误

> 核心结论:HUKS 的本质是"密钥代理"------应用只持有别名,真正的密钥材料从不离开安全硬件,这是 HarmonyOS 安全体系的核心设计。


参考资料