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 安全体系的核心设计。

---

参考资料

- HarmonyOS HUKS 开发指南

- HUKS API Reference - @ohos.security.huks

- cryptoFramework 与 HUKS 配合使用

- OpenHarmony HUKS 源码 - security_huks

- OpenHarmony HUKS Core HAL 实现

Logo

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

更多推荐