HarmonyOS HUKS 密钥全生命周期实战:生成、轮换与失效处理
摘要:在 HarmonyOS 应用中,真正需要保护的通常不是一段加密代码,而是加密密钥本身。本文以本地敏感数据加密为场景,梳理 HUKS 密钥从生成、使用到轮换和销毁的完整生命周期,并给出 ArkTS 封装示例,说明如何处理首次安装、密钥损坏、账号切换和版本升级等工程问题。
为什么"加密了"仍然不安全
把明文改成密文只是第一步。如果密钥硬编码在源码、和密文放在同一个普通文件,或者每次启动都随机生成却没有持久化,攻击者仍然可以通过反编译、文件复制或运行时观察恢复数据。
一个可落地的方案至少要回答四个问题:
- 密钥由谁生成,随机性和存储位置是否可信?
- 应用重启后如何继续使用同一把密钥?
- 密钥泄露或算法升级时,旧数据如何迁移?
- 用户退出、卸载或撤销授权后,密钥如何失效?
HUKS(Huawei Universal KeyStore)适合承担密钥托管职责。应用通过别名找到密钥,但不把原始密钥字节保存到 Preferences、数据库或日志中。
先设计密钥别名和数据格式
密钥别名不要直接使用用户输入,也不要在代码中散落字符串。建议把用途、账号和版本编码到统一策略中:
ts
const KEY_PREFIX = 'profile-data-key'
const CURRENT_VERSION = 1
function keyAlias(accountId: string, version = CURRENT_VERSION): string {
return `${KEY_PREFIX}:${encodeURIComponent(accountId)}:v${version}`
}
别名本身不是秘密,但应避免写入日志。多账号场景要让不同账号使用不同密钥,避免一个账号的导出文件可以直接解密另一个账号的数据。
ts
interface CipherEnvelope {
version: number
algorithm: string
iv: string
ciphertext: string
}
元数据不包含密钥本身。升级算法时,可以根据 version 选择旧密钥解密,再使用新密钥重新加密。
创建和复用 HUKS 密钥
不同 SDK 版本对 HUKS API 的导出方式和参数类型可能有差异,实际项目应以当前 DevEco SDK 定义为准。下面代码展示调用边界和参数意图:
ts
import huks from '@kit.UniversalKeystoreKit'
async function generateKey(alias: string): Promise<void> {
const options: huks.HuksOptions = {
properties: [
{ tag: huks.HuksTag.HUKS_TAG_ALGORITHM, value: huks.HuksKeyAlg.HUKS_ALG_AES },
{ tag: huks.HuksTag.HUKS_TAG_KEY_SIZE, value: 256 },
{ tag: huks.HuksTag.HUKS_TAG_PURPOSE,
value: huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_ENCRYPT |
huks.HuksKeyPurpose.HUKS_KEY_PURPOSE_DECRYPT }
]
}
await huks.generateKeyItem(alias, options)
}
启动时应该先查询别名是否存在:存在则复用,不存在才创建。并发启动时还要处理两个页面同时发现不存在、同时创建的竞争:
ts
async function ensureKey(alias: string): Promise<void> {
if (await hasKey(alias)) return
try {
await generateKey(alias)
} catch (error) {
if (!isAlreadyExistsError(error)) throw error
}
}
hasKey 和 isAlreadyExistsError 应根据当前 SDK 错误码实现,不要依赖错误信息中的文本。实际项目还可以使用进程内 Promise 缓存,减少重复查询和竞争。
加密时必须保存随机 IV
AES 加密通常需要初始化向量(IV)。IV 不需要保密,但同一密钥下不应重复使用。每次加密都生成新的随机 IV,并和密文一起保存:
ts
async function encrypt(alias: string, plain: Uint8Array): Promise<CipherEnvelope> {
const iv = randomBytes(12)
const ciphertext = await huksEncrypt(alias, plain, iv)
return {
version: CURRENT_VERSION,
algorithm: 'AES-256-GCM',
iv: encodeBase64(iv),
ciphertext: encodeBase64(ciphertext)
}
}
推荐使用带认证的加密模式,例如 GCM。它不仅保护内容机密性,还会校验密文是否被修改。解密失败时,应把它视为数据不可验证,而不是尝试把乱码当成有效内容。
ts
async function decrypt(alias: string, envelope: CipherEnvelope): Promise<Uint8Array> {
return huksDecrypt(alias, decodeBase64(envelope.ciphertext), decodeBase64(envelope.iv))
}
IV 和认证标签的格式必须固定并带版本。不要在日志中打印明文、密文或异常对象的完整内容。
把 HUKS 适配层和业务层分开
页面和 Repository 不应该直接拼 HUKS 属性。建议隔离出一个只负责密钥和密码操作的适配器:
ts
class SecureKeyStore {
private readonly initializing = new Map<string, Promise<void>>()
async ensure(alias: string): Promise<void> {
const pending = this.initializing.get(alias)
if (pending) return pending
const task = this.createIfNeeded(alias)
this.initializing.set(alias, task)
try { await task } finally { this.initializing.delete(alias) }
}
private async createIfNeeded(alias: string): Promise<void> {
if (!(await hasKey(alias))) await generateKey(alias)
}
}
业务层只关心适配器的输入输出。这样单元测试可以替换整个适配器,不必在测试环境里依赖真实安全硬件。
轮换不是简单删除旧密钥
密钥轮换的目标是让新数据使用新密钥,同时保证旧数据仍能读取。直接删除旧密钥会让历史数据永久无法解密。
建议采用双读单写策略:
- 生成新版本密钥,保留旧版本密钥。
- 读取数据时根据 envelope 版本选择对应密钥。
- 写入新数据时只使用当前版本密钥。
- 旧数据成功读取后,在合适时机重新加密为新版本。
- 确认没有旧版本数据后,再删除旧密钥。
ts
async function readAndMigrate(
envelope: CipherEnvelope, accountId: string, currentVersion: number
): Promise<Uint8Array> {
const oldAlias = keyAlias(accountId, envelope.version)
const plain = await decrypt(oldAlias, envelope)
if (envelope.version !== currentVersion) {
const newAlias = keyAlias(accountId, currentVersion)
const next = await encrypt(newAlias, plain)
await saveEnvelope(next)
}
return plain
}
迁移必须幂等。保存新密文成功前不能删除旧数据;如果应用在迁移中被杀死,下一次启动仍应能根据旧版本继续处理。大量数据应分批迁移并记录进度,避免启动阶段一次性阻塞。
处理密钥丢失和数据损坏
用户清除应用数据、密钥被系统删除、恢复了没有对应密钥的备份,或数据库 envelope 被截断,都可能导致密钥不可用。
解密失败时不要无限重试,也不要静默生成同名新密钥。新密钥无法解开旧密文,静默覆盖会造成不可恢复的数据损失。
ts
type SecureDataError =
| 'KEY_NOT_FOUND'
| 'AUTHENTICATION_FAILED'
| 'MALFORMED_ENVELOPE'
| 'TEMPORARY_PROVIDER_ERROR'
KEY_NOT_FOUND:提示用户数据无法恢复,必要时清理无效密文并重新初始化。AUTHENTICATION_FAILED:认为数据被修改或密钥不匹配,不展示敏感细节。MALFORMED_ENVELOPE:记录格式版本和字段缺失,走修复或降级流程。TEMPORARY_PROVIDER_ERROR:有限次数重试,并提供稍后再试的界面。
错误上报只携带错误类别、应用版本、数据格式版本等非敏感信息,禁止上传密钥材料。
用户退出和数据清理
账号 ID 应纳入密钥别名。账号切换时,必须避免旧账号数据被新账号读取:
ts
async function signOut(accountId: string): Promise<void> {
await clearAccountSession(accountId)
await clearInMemoryPlaintext()
// 按产品策略保留离线密文,或删除密文与对应密钥。
}
如果产品要求退出即清除,应先删除密文,再删除对应版本的 HUKS 密钥,并清除内存缓存。删除失败时要保留可重试状态,不能只删除数据库记录而遗留密钥。
无法随应用备份恢复的 HUKS 密钥对应的密文,不应被当成可恢复数据上传云端。可以排除密文,或在恢复后让用户重新建立本地安全容器。
测试与验收清单
- 首次启动能够创建密钥,重复启动不会覆盖已有密钥。
- 两个页面同时初始化同一别名时只完成一次创建。
- 每次加密使用不同 IV,解密能够校验认证标签。
- 错误的 IV、密文或密钥版本会明确失败。
- 新版本密钥轮换后,旧数据仍可读取并完成迁移。
- 迁移中途进程退出,重试不会破坏旧数据。
- 删除密钥后,应用不会静默生成新密钥覆盖旧数据。
- 用户切换账号后无法读取上一个账号的密文。
- 日志、埋点和崩溃上报不包含明文、密文和密钥材料。
总结
HUKS 的价值不只是把密钥放到系统里,更重要的是建立完整的密钥生命周期:用稳定别名定位用途,用随机 IV 和认证加密保护每次数据,用版本号支持轮换,用明确错误类型处理失效,并在用户退出和数据清理时收敛边界。
密钥管理最终保护的是数据可用性和用户信任,不能只验证"能否加密",还要验证"密钥变化后能否安全恢复或明确失败"。