【鸿蒙】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 放到数组末尾,在低版本固件上触发。 解决:按官方文档示例中属性的固定顺序书写,始终将 ALGORITHM → PURPOSE → KEY_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 安全体系的核心设计。
---
参考资料
- HUKS API Reference - @ohos.security.huks
更多推荐


所有评论(0)