鸿蒙HarmonyOS AI 应用安全红线实战 —— API Key 管理、网络安全、工具安全、发布扫描
一、前言:AI 应用安全的"五条红线"
做 AI Agent 应用,有五条安全红线绝不能碰:
-
Key 不落盘:API Key 只在进程内存,永不写入文件/数据库/preferences/rawfile/日志。
-
Key 不入 State:AgentState 持久化时,Key 绝不被序列化。
-
HTTPS only:所有网络请求走 HTTPS,禁止明文 HTTP,禁止重定向到非 HTTPS。
-
工具最小权限:工具默认只读,危险操作需用户确认,ToolContext 不暴露 Key。
-
发布前扫描:HAP 包发布前必须通过密钥扫描,确认无泄漏。
这五条红线看似简单,但在实际工程中每一条都有自己的陷阱。ArkAgent 用 RuntimeCredentials + BearerTokenProvider + UrlUtil + scan-hap-secrets.sh + security-baseline.md 构建了完整的安全防护体系。
二、API Key 生命周期
2.1 RuntimeCredentials:内存专用持有器
ArkAgent 示例工程的 RuntimeCredentials(entry/src/main/ets/session/RuntimeCredentials.ets)是 Key 管理的最佳实践参考:
/**
* Keys live only in process memory. They are never written to filesDir,
* preferences, rawfile, or logs. Call clearAll() on Ability destroy and
* when the page tears down.
*/
export class RuntimeCredentials {
private static instance?: RuntimeCredentials
private zhipuApiKey: string = ''
private deepseekApiKey: string = ''
private zhipuModel: string = 'glm-5.2' // 模型 ID 非敏感
private deepseekModel: string = 'deepseek-v4-flash'
static shared(): RuntimeCredentials { ... }
/** Does not log, persist, or echo the value. */
setApiKey(provider: string, key: string): void {
const trimmed = key.trim()
if (provider === 'deepseek') { this.deepseekApiKey = trimmed }
else { this.zhipuApiKey = trimmed }
}
/** Callers must not log, persist, or put this into AgentState / UI snapshots. */
getApiKey(provider: string): string { ... }
/** Only returns "已输入(仅内存)" or "未输入" — never the key content. */
keyStatusLabel(provider: string): string { ... }
/** Zero out keys. Call on Ability destroy. */
clearAll(): void {
this.zhipuApiKey = ''
this.deepseekApiKey = ''
}
}
2.2 Key 存储位置红线
|
位置 |
能不能存 Key |
原因 |
|---|---|---|
|
rawfile/ |
❌ |
打入 HAP,可被解包提取 |
|
preferences |
❌ |
明文存储在设备 filesDir |
|
filesDir 文件 |
❌ |
设备备份可被提取 |
|
AgentState |
❌ |
State 会被持久化到文件 |
|
日志(hilog) |
❌ |
日志可被收集 |
|
错误信息 |
❌ |
可能显示给用户或记入日志 |
|
Git 提交 |
❌ |
版本控制泄漏 |
|
进程内存 |
✅ |
唯一允许的位置 |
2.3 页面集成中的密钥流转
// 用户输入 Key 后立即清空 UI 缓冲
private applyRuntimeKey(): void {
SessionCoordinator.shared().setRuntimeApiKey(this.apiKeyInput)
this.apiKeyInput = '' // ← 立即清空
this.refreshKeyStatus()
}
// 页面销毁清零
aboutToDisappear(): void {
this.apiKeyInput = ''
SessionCoordinator.shared().clearSecrets()
}
Key 输入后立即清空 UI 输入框缓冲,不让 Key 在 @State 里多留一秒。
2.4 ⚠️ 踩坑一:rawfile 打包 Key 进 HAP
症状:阶段 9 验收发现 local key 可能进入 HAP——rawfile 会打包进应用资源,HAP 安装包里直接包含 Key 文件。
根因:如果把 provider_keys.local.json 放在 entry/src/main/resources/rawfile/ 下,HAP 打包时会把它打入资源包。任何人解包 HAP 就能提取 Key。
修复:
-
禁止在 main rawfile 存放任何 key 文件
-
Key 只通过运行时密码输入(内存)
-
发布前执行
scan-hap-secrets.sh扫描 HAP 包
阶段 9 报告踩坑表记录:"local key 进入 HAP:rawfile 会打包进资源 → 禁止 main rawfile Key;运行时内存输入 + secret scan"。
2.4 ⚠️ 踩坑二:错误信息拼接 Authorization 泄漏
症状:网络请求失败时,错误信息里拼接了完整的请求头(含 Authorization: Bearer xxx),日志收集系统记录了这个错误,Key 泄漏。
根因:错误处理代码直接把请求的 cause 信息透传给上层,没有做脱敏。HTTP 库的错误消息可能包含完整的请求 URL 和 Header。
修复:所有可能包含 Key 的错误信息都做 Bearer *** 替换:
// HarmonyHttpTransport 和 OpenAICompatibleClient 的错误处理
const sanitized = message.replace(/Bearer\s+[A-Za-z0-9._\-]+/gi, 'Bearer ***')
Eval 模块更进一步,用 EvalSanitize.redactString 做多模式脱敏:Bearer ***、sk-***、data:***;base64,***、签名 query、Basic ***。
三、密钥注入:BearerTokenProvider
3.1 SPI 接口
export interface BearerTokenProvider {
getToken(): Promise<string>
}
export class StaticBearerTokenProvider implements BearerTokenProvider {
private readonly token: string
constructor(token: string) { this.token = token }
async getToken(): Promise<string> { return this.token }
}
3.2 注入点
// OpenAICompatibleClient.buildHttpRequest
const token = await this.config.tokenProvider.getToken()
if (token.trim().length === 0) {
throw ArkAgentError.config('missing_token', 'Bearer token is empty')
}
headers.set('Authorization', `Bearer ${token}`)
Core 不持久化 token——Token 通过 ProviderConfig 的 tokenProvider 注入,只在网络调用时取出使用,不进入 ModelConfig、不进入 AgentState、不进入日志。
四、网络安全
4.1 HTTPS only
// UrlUtil
export function ensureHttps(url: string): Result<void, ArkAgentError> {
if (!url.startsWith('https://')) {
return Result.failure(ArkAgentError.security('https_required',
'Only HTTPS endpoints are allowed'))
}
return Result.success(undefined as void)
}
每次请求/stream 前都调 UrlUtil.ensureHttps 校验——HarmonyHttpTransport 的 validateRequest 里做了这道门。
4.2 禁止重定向
security-baseline.md 明确:"禁止自动跟随非 HTTPS 或跨主域重定向"。HarmonyHttpTransport 没有任何 followRedirect 选项——系统 http 模块默认不跟随,且 security-baseline 禁止开启。
4.3 Proxy 硬失败
private ensureProxyNotConfigured(): void {
if (this.config.proxyUrl !== undefined && this.config.proxyUrl.trim().length > 0) {
throw ArkAgentError.config('proxy_unsupported',
'HTTP proxy is not supported by HarmonyHttpTransport; leave proxyUrl empty')
}
}
鸿蒙 @ohos.net.http 不支持应用层 HTTP 代理。设置 proxyUrl 是硬性配置错误(不静默忽略)。
4.4 错误信息脱敏
// HarmonyHttpTransport.mapTransportError
// Never include authorization or bodies in cause.
const sanitized = message.replace(/Bearer\s+[A-Za-z0-9._\-]+/gi, 'Bearer ***')
return ArkAgentError.transport('http_transport_error', 'HTTP transport failure',
ErrorRetryability.conditional, sanitized)
所有可能包含 Key 的错误信息都做了 Bearer *** 替换。OpenAICompatibleClient 的 asError 方法也做了同样的脱敏。
五、HeaderPolicy:敏感头脱敏
5.1 RESERVED headers
static readonly RESERVED: string[] = [
'authorization', 'content-type', 'accept', 'host',
'content-length', 'transfer-encoding', 'connection', 'proxy-authorization'
]
RESERVED headers 不可被 custom headers 覆盖。mergeCustomHeaders 撞到 RESERVED 名返回 reserved_header 错误——不静默丢弃,显式报错。
5.2 SENSITIVE 脱敏
static readonly SENSITIVE_NAME_HINTS: string[] = [
'authorization', 'api-key', 'apikey', 'x-api-key',
'token', 'secret', 'password', 'cookie', 'set-cookie'
]
static redactHeadersForLog(headers: HttpHeaders): JsonObject {
// 匹配 SENSITIVE_NAME_HINTS 的 header → 值替换为 ***
}
所有日志输出的 header 都经 redactHeadersForLog 脱敏。
六、工具安全
6.1 最小权限
security-baseline.md:
-
"Tool 默认最小权限;读、写、删除、支付、发送、系统设置分级。"
-
"危险 Tool 在 beforeToolCall 要求用户确认。"
6.2 ToolContext 不暴露 Key
"ToolContext 不暴露 API Key 和任意 Service Locator。"
ToolContext 只有:sessionId(只读 state)、batchCallId、signal、services(有限白名单 ToolServiceRegistry)。没有 Key、没有任意 Service Locator。
6.3 路径遍历防护
// safeSessionFileName
// 拒绝空、..、/、\\、控制字符
// 非安全字符替换为 _
State 文件路径用 safeSessionFileName 校验,防止 ../../etc/passwd 作为 sessionId 读取任意文件。
七、scan-hap-secrets.sh:发布前扫描
7.1 四类扫描
# scripts/scan-hap-secrets.sh
# "Fails if any of the following appear in main resources or packaged HAP:
# provider_keys.local.json... Authorization/Bearer headers with non-redacted material...
# placeholder/sentinel key strings...
# Does NOT print secret values. Hits are reported by path + pattern name only."
|
扫描类型 |
检查内容 |
|---|---|
|
main rawfile |
禁 |
|
HAP 文件名 |
禁止含密钥文件名 |
|
placeholder/sentinel |
禁 |
|
非脱敏 Bearer |
禁 |
7.2 验收证据
阶段 9 报告:"scan-hap-secrets.sh OK clean(主 HAP 无 local key 文件 / placeholder / 非脱敏 Bearer)"。
八、EvalSanitize:评测脱敏
Eval 模块的 EvalSanitize.redactString 做了多模式脱敏:
// Bearer *** (Authorization Header)
// sk-*** (API Key 前缀 sk-)
// data:***;base64,*** (Base64 数据)
// 签名 query(Signature|X-Amz-Signature|token|access_token|Expires)
// Basic *** (Basic Auth)
所有 Trace/Recording/Report 出口都经此脱敏——保证评测产物不泄漏敏感信息。
九、风险登记册
ArkAgent 的 risk-register.md 记录了 12 条风险:
|
ID |
风险 |
严重度 |
状态 |
|---|---|---|---|
|
R-002 |
签名凭据进入版本控制 |
严重 |
开放 |
|
R-003 |
ArkTS 递归 JSON 类型 |
中 |
已缓解 |
|
R-004 |
HarmonyOS HTTP 流被缓冲 |
高 |
开放(SPI 逃生通道) |
|
R-005 |
Provider 兼容协议漂移 |
中 |
已缓解(90 天复验) |
|
R-006 |
任意 JS 无安全沙箱 |
严重 |
1.0 不实现(ADR-0012) |
|
R-007 |
暂停恢复重复 Tool 副作用 |
严重 |
已缓解(配对 checkpoint) |
|
R-009 |
reasoning/用户数据进入遥测 |
高 |
已缓解(脱敏) |
|
R-010 |
Sub-agent 资源费用失控 |
高 |
开放(预算限制) |
|
R-012 |
多模态大对象内存压力 |
高 |
开放(大小限制+URI) |
关键设计:风险登记册不假装风险已解决——R-004(HTTP 缓冲)至今保持"开放"状态,用 Transport SPI 留逃生通道。这种"诚实面对未知"的工程态度是安全的基础。
十、security-baseline.md 发布门禁
6 条发布安全门禁:
-
secret 扫描无高风险
-
危险 Tool 确认测试
-
路径穿越测试
-
脱敏测试
-
依赖许可证清单
-
在线 Provider 测试无请求内容泄漏
十一、最佳实践清单
密钥管理
-
✅ Key 只在运行时内存输入(密码框),不写入任何持久化存储。
-
✅ Key 输入后立即清空 UI 缓冲。
-
✅ 页面 aboutToDisappear / Ability onDestroy 调用 clearAll()。
-
✅ Key 通过 BearerTokenProvider 注入,不进入 ModelConfig/AgentState。
-
❌ 不把 Key 写入 rawfile/preferences/filesDir/Git。
网络安全
-
✅ 所有请求 HTTPS only(UrlUtil.ensureHttps)。
-
✅ 禁止重定向到非 HTTPS。
-
✅ Proxy 硬失败(不静默忽略)。
-
✅ 错误信息 Bearer *** 脱敏。
工具安全
-
✅ 工具默认最小权限。
-
✅ 危险工具标 ToolRiskLevel.dangerous + 审批。
-
✅ ToolContext 不暴露 Key/任意 Service Locator。
-
✅ 路径参数用 safeSessionFileName 校验。
发布安全
-
✅ 发布前跑 scan-hap-secrets.sh。
-
✅ 日志 header 经 redactHeadersForLog 脱敏。
-
✅ Eval 产物经 EvalSanitize 脱敏。
-
✅ RESERVED headers 不可被 custom headers 覆盖。
十二、常见错误对照表
|
错误做法 |
问题 |
正确做法 |
|---|---|---|
|
Key 写入 rawfile |
HAP 可被解包提取 |
运行时内存输入 |
|
Key 留在 UI @State |
页面销毁前暴露 |
应用后立即清空 |
|
AgentState 存 Key |
持久化泄漏密钥 |
State 只存可序列化非敏感数据 |
|
日志输出 Authorization |
日志收集泄漏 |
Bearer *** 脱敏 |
|
错误信息拼接请求体 |
泄漏 prompt/Header |
只显示错误码和简短说明 |
|
HTTP 明文传输 |
中间人攻击 |
HTTPS only |
|
自动跟随重定向 |
钓鱼重定向 |
禁止重定向 |
|
proxyUrl 静默忽略 |
行为不可预测 |
硬性 config error |
|
工具能访问 Key |
Key 泄漏风险 |
ToolContext 不暴露 Key |
|
危险工具自动执行 |
不可逆操作 |
ToolRiskLevel.dangerous + 审批 |
|
sessionId 不校验路径 |
路径遍历读任意文件 |
safeSessionFileName |
|
发布不扫密钥 |
HAP 内残留 Key |
scan-hap-secrets.sh |
十三、验证清单
密钥
-
Key 输入后输入框立即清空
-
退出页面后 Key 不保留
-
AgentState 持久化文件中无 API Key
-
错误信息不含 Key / Authorization
-
日志不含 Key / Authorization
网络
-
所有请求 HTTPS
-
HTTP URL 被拒绝
-
无重定向
-
proxyUrl 被拒绝
工具
-
ToolContext 无 Key
-
危险工具触发审批
-
路径参数防遍历
发布
-
scan-hap-secrets.sh 无命中
-
HAP 无 placeholder/sentinel
-
HAP 无非脱敏 Bearer
-
设备 filesDir 无 Key 文件
十四、构建验证
# 构建后扫描
NODE_HOME=/Applications/DevEco-Studio.app/Contents/tools/node \
DEVECO_SDK_HOME=/Applications/DevEco-Studio.app/Contents/sdk \
/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw assembleHap --no-daemon
# 密钥扫描
bash scripts/scan-hap-secrets.sh
scan-hap-secrets.sh: OK clean
十五、写在最后
Key 只进内存不落盘,页面销毁就清零。 BearerTokenProvider 注入,State 日志永不存。 HTTPS only 不重定向,proxy 硬失败不留情。 错误信息不拼体,Bearer 星号来脱敏。 RESERVED 头不可盖,敏感头日志打星号。 工具最小权限走,危险操作要审批。 ToolContext 不露 Key,Service 白名单来把关。 sessionId 防遍历,safeSessionFileName 保平安。 发布之前扫密钥,scan-hap-secrets 不能少。 风险登记不假装,开放风险留通道。
本文是 ArkAgent 鸿蒙教程系列的第十四篇,也是最后一篇。从架构全景到快速接入,从流式输出到 JSON 类型安全,从工具调用到状态持久化,从控制安全到记忆子 Agent,从技能规划到评估框架,从测试策略到安全红线——十四篇文章构成了鸿蒙 AI Agent SDK 开发的完整知识体系。如果你正在鸿蒙上做 AI 应用,可以把这套架构和设计决策直接作为你的工程基线。
更多推荐




所有评论(0)