一、前言: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.shscan-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 应用,可以把这套架构和设计决策直接作为你的工程基线。