- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
导读
本文基于 Operit 仓库中的《Codex Auth Keystore Restore Crash》技术方案(docs/TODO/codex_auth_keystore_restore_20260904/index.md),完整还原 Android 上EncryptedSharedPreferences存储 Codex OAuth 凭据时,因 Android Auto Backup / 设备迁移导致 Keystore 密钥失配、抛出AEADBadTagException崩溃的根因与双层修复方案。读完本文,你将掌握:为什么加密 SharedPreferences 不能参与系统备份、如何在backup_rules.xml与data_extraction_rules.xml中精准排除敏感凭据文件,以及如何在应用层实现"限定清理 + 优雅回到未登录态"的兜底恢复,让模型设置页在任何设备恢复场景下都能正常打开。
一、问题背景:Codex OAuth 凭据的加密存储方式
1.1 凭据存储载体:EncryptedSharedPreferences
Operit 通过CodexAuthPreferences(app/src/main/java/com/ai/assistance/operit/data/preferences/CodexAuthPreferences.kt)统一保管所有 Codex 模型配置共用的 OAuth 凭据。它使用 AndroidX Security 库的EncryptedSharedPreferences,存储名为codex_oauth_credentials:
private fun createEncryptedPreferences(context: Context): SharedPreferences { return EncryptedSharedPreferences.create( context, STORE_NAME, // "codex_oauth_credentials" MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(), EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, ) }可见其加密体系为:MasterKey采用AES256_GCM密钥方案,键(key)采用AES256_SIV加密,值(value)采用AES256_GCM加密(见 CodexAuthPreferences.kt)。
1.2 存储的凭据字段
CodexAuthPreferences内部以CodexAuthState数据类承载全部凭据信息(CodexAuthPreferences.kt):
| 字段 | 说明 | SharedPreferences Key |
|---|---|---|
accessToken | OAuth 访问令牌(写入前校验非空) | access_token |
refreshToken | OAuth 刷新令牌(写入前校验非空) | refresh_token |
expiresAtMillis | 访问令牌过期时间戳(必须 > 0) | expires_at |
accountId | ChatGPT 账户 ID(写入前校验非空) | account_id |
residency | 账户区域信息(可空,为空时移除 key) | residency |
email | 账户邮箱(可空,为空时移除 key) | email |
save()方法(CodexAuthPreferences.kt)通过require(...)对四个必填字段做前置校验,保证落盘的凭据始终结构完整;写入后同步更新MutableStateFlow,供 UI 层以StateFlow<CodexAuthState?>方式订阅登录态变化。
1.3 凭据写入链路
Codex OAuth 登录完成后,凭据并非直接写入 Preferences,而是经过CodexAuthManager.saveLoginTokens()(app/src/main/java/com/ai/assistance/operit/data/api/CodexAuthManager.kt)解析 JWT claims 后组装CodexAuthState再持久化:
- 从
accessToken与idToken的 JWT claims 中解析accountId、expiresAtMillis、residency、email; - 令牌刷新窗口为
REFRESH_WINDOW_MILLIS = 5 * 60 * 1000L,即过期前 5 分钟内getValidAccessToken()才会走refreshMutex保护的刷新流程(CodexAuthManager.kt); - 登出时先尝试向服务端 revoke refresh token,再执行
preferences.clear()(CodexAuthManager.kt)。
二、崩溃根因:Keystore 密钥设备绑定 vs 备份复制 XML
2.1 现象与触发路径
文档《原状》一节描述的现象是:Android Auto Backup 与设备迁移会复制shared_prefs目录下的 XML 文件,但 Android Keystore 中的密钥仍然绑定在原设备上。恢复后的 Tink keyset 无法通过新设备密钥校验,于是当用户打开模型设置页时抛出AEADBadTagException。
在 Operit 中,这一崩溃的暴露点正是模型 API 设置区块:ModelApiSettingsSection通过CodexAuthManager.getInstance(context).authState收集登录态(app/src/main/java/com/ai/assistance/operit/ui/features/settings/sections/ModelApiSettingsSection.kt),一旦CodexAuthPreferences在构造阶段打开损坏的加密 store 失败,设置页即无法正常渲染。
2.2 根本原因链
EncryptedSharedPreferences底层由 Tink 管理加密 keyset;- Tink 的 keyset 由 Android Keystore 中的非导出密钥加密保护;
- Android Keystore 密钥是设备本地的(device-local),不会随备份恢复迁移;
- Auto Backup 把
codex_oauth_credentials.xml(内含加密后的 keyset 与数据)原样复制到新设备; - 新设备上没有原设备的 Keystore 条目,Tink 解密 keyset 失败,表现为
AEADBadTagException; - 若不在读取入口捕获该异常,设置页直接崩溃。
这一机制意味着:"密钥本地、数据可迁移"的不对称性是加密凭据参与系统备份的固有风险,必须从备份规则与运行时恢复两个层面同时处理。
三、修复方案一:备份规则排除敏感凭据文件
3.1 Manifest 挂载两个规则文件
Operit 在 app/src/main/AndroidManifest.xml 中同时挂载了两套备份规则,覆盖 API 31 前后两代备份机制:
android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules"3.2 legacy full-backup:backup_rules.xml
app/src/main/res/xml/backup_rules.xml 服务于 API 31 以下设备使用的full-backup-content机制,直接排除凭据文件:
<full-backup-content> <!-- EncryptedSharedPreferences depends on a device-local Android Keystore key. --> <exclude domain="sharedpref" path="codex_oauth_credentials.xml" /> </full-backup-content>关键点:domain="sharedpref"指定备份域为 SharedPreferences,path精确指向CodexAuthPreferences使用的存储文件名(与源码中STORE_NAME = "codex_oauth_credentials"一一对应)。
3.3 新一代数据提取规则:data_extraction_rules.xml
API 31+ 使用 app/src/main/res/xml/data_extraction_rules.xml,其将备份场景拆分为云备份(cloud-backup)与设备间迁移(device-transfer),两个场景都必须排除:
<data-extraction-rules> <cloud-backup> <!-- EncryptedSharedPreferences depends on a device-local Android Keystore key. --> <exclude domain="sharedpref" path="codex_oauth_credentials.xml" /> </cloud-backup> <device-transfer> <!-- EncryptedSharedPreferences depends on a device-local Android Keystore key. --> <exclude domain="sharedpref" path="codex_oauth_credentials.xml" /> </device-transfer> </data-extraction-rules>device-transfer场景对应"换机迁移(手机到手机)";只排除cloud-backup而不排除device-transfer会留下迁移后崩溃的隐患,因此两处必须同时配置。
四、修复方案二:运行时限定清理兜底
备份规则只能阻止今后的备份携带凭据;对已经损坏的 encrypted store,仍需要应用层的自愈逻辑。Operit 在CodexAuthPreferences中做了双层防御。
4.1 读取阶段的 SecurityException 兜底
readState()(CodexAuthPreferences.kt)是首次读取登录态的入口,专门捕获SecurityException:
private fun readState(): CodexAuthState? { return try { readStateFromPreferences() } catch (error: SecurityException) { // Restored encrypted values can be unreadable when Android Keystore kept the key // device-local. Reset only Codex OAuth state so the settings screen can open. AppLogger.e(TAG, "Codex OAuth credentials are unreadable; resetting encrypted store", error) resetEncryptedStore() null } }4.2 构造阶段的 GeneralSecurityException / IOException 兜底
即使readState()未触发,createPreferences()(CodexAuthPreferences.kt)在创建EncryptedSharedPreferences时也会捕获GeneralSecurityException与IOException——这正是 Tink 拒绝损坏 keyset(AEADBadTagException属于GeneralSecurityException族)时实际抛出的路径:
private fun createPreferences(context: Context): SharedPreferences { return try { createEncryptedPreferences(context) } catch (error: GeneralSecurityException) { recreatePreferencesAfterUnreadableStore(context, error) } catch (error: IOException) { recreatePreferencesAfterUnreadableStore(context, error) } } private fun recreatePreferencesAfterUnreadableStore( context: Context, error: Exception ): SharedPreferences { // Android backup restores SharedPreferences XML but not the app's Android Keystore // entry. Tink then rejects the encrypted keyset with AEADBadTagException. AppLogger.e(TAG, "Codex OAuth encrypted store cannot be opened; resetting it", error) context.deleteSharedPreferences(STORE_NAME) return createEncryptedPreferences(context) }4.3 限定清理的实现语义
resetEncryptedStore()(CodexAuthPreferences.kt)只做两件事:
private fun resetEncryptedStore() { appContext.deleteSharedPreferences(STORE_NAME) preferences = createEncryptedPreferences(appContext) }deleteSharedPreferences(STORE_NAME)仅删除codex_oauth_credentials.xml这一个文件;- 随后立刻重建一个全新的空加密 store,保证后续
save()/clear()调用不会因引用失效的 Preferences 实例而再次崩溃。
这正是文档《预期》中"限定清理"(scoped cleanup)的含义:只重置 Codex OAuth 凭据,其他 SharedPreferences、DataStore 和数据库完全不受影响。用户看到的最终结果是回到未登录状态,模型设置页可正常打开,并可重新发起登录。
五、修复后的用户流程与验证要点
5.1 崩溃自愈后的重新登录路径
清除损坏凭据后,用户可通过CodexOAuthCoordinator(app/src/main/java/com/ai/assistance/operit/ui/features/codex/CodexOAuthCoordinator.kt)重新走完整 OAuth 流程:
- 启动本机 loopback 回调服务器
CodexOAuthLoopbackCallbackServer.open(); - 生成 PKCE 码对与防 CSRF 的
state参数; - 构造授权 URL 并唤起登录;
- 回调后校验
state一致性、检查error参数、提取授权码; - 用授权码换取 token,交由
CodexAuthManager.saveLoginTokens()写入全新的加密 store。
5.2 验证清单
| 验证项 | 预期结果 | 依据 |
|---|---|---|
| 打开模型设置页 | 不崩溃,Codex 区块正常渲染 | ModelApiSettingsSection收集authState(ModelApiSettingsSection.kt) |
| 损坏 store 存在时启动 | 自动清理,登录态为 null,显示登录入口(ModelApiSettingsSection.kt) | readState()/createPreferences()双兜底 |
| 重新登录 | 凭据写入新加密 store,登录态恢复 | CodexAuthManager.saveLoginTokens() |
| 其他偏好数据 | 完整保留 | deleteSharedPreferences仅作用于codex_oauth_credentials.xml |
| 云备份 / 换机迁移 | 不再携带凭据 XML | data_extraction_rules.xml双场景 exclude |
六、适用范围与通用启示
本方案的技术结论可推广到 Android 上所有使用EncryptedSharedPreferences存储敏感凭据的场景:
- 凡是依赖 Android Keystore 的加密数据,都不应参与系统级备份。
MasterKey派生密钥、Tink keyset 均为设备本地产物,备份 XML 而不备份密钥必然导致恢复后解密失败; - 排除规则必须双轨配置:API 31 以下走
fullBackupContent(backup_rules.xml),API 31+ 走dataExtractionRules(data_extraction_rules.xml),且cloud-backup与device-transfer都要覆盖; - 运行时兜底与备份规则互为保险:备份规则防患于未然,运行时捕获
SecurityException/GeneralSecurityException/IOException并执行限定清理,则能处理历史上已损坏的存量数据; - 清理粒度要克制:仅删除目标 store 文件并重建空实例,避免波及同目录下其他偏好、DataStore 与数据库,是保证自愈功能安全性的关键设计。
如需深入了解实现细节,可继续阅读仓库中的相关源码:CodexAuthPreferences.kt、CodexAuthManager.kt、backup_rules.xml 与 data_extraction_rules.xml。
- AI Agent
- 人工智能
- 大模型
- AI 应用
- 工具调用
- 本地部署
- MCP Clients
- Agent 记忆
【免费下载链接】Operit
The most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent
相关推荐
Valetudo故障恢复:系统崩溃后的修复方法
Valetudo故障恢复:系统崩溃后的修复方法 你是否遇到过Valetudo系统突然崩溃、机器人无法响应的情况?当扫地机器人失去控制,清洁计划被迫中断时,及时的
物联网后端前端Operit 模块迁移后的 DragonBones CI 依赖路径恢复:CMake FetchContent 与 PR 分类修正实战
Operit 模块迁移后的 DragonBones CI 依赖路径恢复:CMake FetchContent 与 PR 分类修正实战 本文整理自仓库文档 dra
AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化ReactOS灾难恢复:系统崩溃后的修复与数据抢救全指南
ReactOS灾难恢复:系统崩溃后的修复与数据抢救全指南 ReactOS作为一款免费的Windows兼容操作系统,为用户提供了稳定的使用体验。但系统崩溃等问题仍
操作系统内核驱动驱动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考