Operit 设备迁移后 Codex OAuth 崩溃修复指南:EncryptedSharedPreferences 与 Android Keystore 恢复边界 AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载导读本文基于 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 凭据存储载体EncryptedSharedPreferencesOperit 通过CodexAuthPreferencesapp/src/main/java/com/ai/assistance/operit/data/preferences/CodexAuthPreferences.kt统一保管所有 Codex 模型配置共用的 OAuth 凭据。它使用 AndroidX Security 库的EncryptedSharedPreferences存储名为codex_oauth_credentialsprivate 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 KeyaccessTokenOAuth 访问令牌写入前校验非空access_tokenrefreshTokenOAuth 刷新令牌写入前校验非空refresh_tokenexpiresAtMillis访问令牌过期时间戳必须 0expires_ataccountIdChatGPT 账户 ID写入前校验非空account_idresidency账户区域信息可空为空时移除 keyresidencyemail账户邮箱可空为空时移除 keyemailsave()方法CodexAuthPreferences.kt通过require(...)对四个必填字段做前置校验保证落盘的凭据始终结构完整写入后同步更新MutableStateFlow供 UI 层以StateFlowCodexAuthState?方式订阅登录态变化。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 备份复制 XML2.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 管理加密 keysetTink 的 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:dataExtractionRulesxml/data_extraction_rules android:fullBackupContentxml/backup_rules3.2 legacy full-backupbackup_rules.xmlapp/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 domainsharedpref pathcodex_oauth_credentials.xml / /full-backup-content关键点domainsharedpref指定备份域为 SharedPreferencespath精确指向CodexAuthPreferences使用的存储文件名与源码中STORE_NAME codex_oauth_credentials一一对应。3.3 新一代数据提取规则data_extraction_rules.xmlAPI 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 domainsharedpref pathcodex_oauth_credentials.xml / /cloud-backup device-transfer !-- EncryptedSharedPreferences depends on a device-local Android Keystore key. -- exclude domainsharedpref pathcodex_oauth_credentials.xml / /device-transfer /data-extraction-rulesdevice-transfer场景对应换机迁移手机到手机只排除cloud-backup而不排除device-transfer会留下迁移后崩溃的隐患因此两处必须同时配置。四、修复方案二运行时限定清理兜底备份规则只能阻止今后的备份携带凭据对已经损坏的 encrypted store仍需要应用层的自愈逻辑。Operit 在CodexAuthPreferences中做了双层防御。4.1 读取阶段的 SecurityException 兜底readState()CodexAuthPreferences.kt是首次读取登录态的入口专门捕获SecurityExceptionprivate 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 拒绝损坏 keysetAEADBadTagException属于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 apps 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 崩溃自愈后的重新登录路径清除损坏凭据后用户可通过CodexOAuthCoordinatorapp/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收集authStateModelApiSettingsSection.kt损坏 store 存在时启动自动清理登录态为 null显示登录入口ModelApiSettingsSection.ktreadState()/createPreferences()双兜底重新登录凭据写入新加密 store登录态恢复CodexAuthManager.saveLoginTokens()其他偏好数据完整保留deleteSharedPreferences仅作用于codex_oauth_credentials.xml云备份 / 换机迁移不再携带凭据 XMLdata_extraction_rules.xml双场景 exclude六、适用范围与通用启示本方案的技术结论可推广到 Android 上所有使用EncryptedSharedPreferences存储敏感凭据的场景凡是依赖 Android Keystore 的加密数据都不应参与系统级备份。MasterKey派生密钥、Tink keyset 均为设备本地产物备份 XML 而不备份密钥必然导致恢复后解密失败排除规则必须双轨配置API 31 以下走fullBackupContentbackup_rules.xmlAPI 31 走dataExtractionRulesdata_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 ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载相关推荐Valetudo故障恢复系统崩溃后的修复方法Valetudo故障恢复系统崩溃后的修复方法 你是否遇到过Valetudo系统突然崩溃、机器人无法响应的情况当扫地机器人失去控制清洁计划被迫中断时及时的物联网后端前端Operit 模块迁移后的 DragonBones CI 依赖路径恢复CMake FetchContent 与 PR 分类修正实战Operit 模块迁移后的 DragonBones CI 依赖路径恢复CMake FetchContent 与 PR 分类修正实战 本文整理自仓库文档 draAI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化ReactOS灾难恢复系统崩溃后的修复与数据抢救全指南ReactOS灾难恢复系统崩溃后的修复与数据抢救全指南 ReactOS作为一款免费的Windows兼容操作系统为用户提供了稳定的使用体验。但系统崩溃等问题仍操作系统内核驱动驱动开发上一篇KoReader墨水屏电子书阅读器PDF、EPUB、DjVu 全格式一次搞定下一篇Streamio-FFmpeg异常处理与错误调试解决视频处理常见问题创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考