OpenHuman 密钥存储内核解析:keyring 域的架构设计、后端选择与 ChaCha20-Poly1305 加密迁移机制
OpenHuman 密钥存储内核解析keyring 域的架构设计、后端选择与 ChaCha20-Poly1305 加密迁移机制【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman本文以 keyring 域说明文档 为主体结合其源码实现系统拆解 OpenHuman面向 Mac / Windows / Linux 的本地优先个人 AI 应用中密钥到底存在哪里、用什么算法加密、如何在不同存储形态之间无损迁移这一整套基础设施设计。读完你将掌握四种可插拔密钥存储后端的选择与冻结规则、SecretStore配置字段加密enc2:/ 遗留enc:格式的完整迁移路径、跨进程文件锁与原子写等并发安全机制以及 OpenHuman 如何保证多用户密钥互不碰撞、测试与生产环境完全隔离。OpenHuman 是一个以本地优先为理念的开源个人 AI 应用其核心Rust中凡是涉及 API Key、Token、钱包助记词、会话凭证等敏感数据的读写都汇聚在一个独立的基础设施域——src/openhuman/security/keyring。该域本身不暴露任何 RPC 控制器、不提供任何 Agent 工具、也不订阅任何事件总线它是一个被其他域在进程内直接消费的叶子模块向上提供统一命名的get/set/delete接口向下屏蔽 macOS Keychain、Windows Credential Manager、Linux Secret Service 以及各类加密/明文文件后端的差异并用 ChaCha20-Poly1305 认证加密为配置中的敏感字段提供纵深防御。一、模块定位一个无外部接口的基础设施域keyring 域在 OpenHuman 安全架构中的角色非常清晰——它不面向用户或 Agent 提供任何独立能力而是为钱包、凭证、设备密钥、配置加载等模块提供底层存储原语。README 与源码共同确认了这一点无 RPC / 控制器该域没有schemas.rs不注册任何openhuman.keyring_*方法无 Agent 工具Agent 无法直接调用 keyring 的任何函数无事件没有bus.rs不发布也不订阅任何DomainEvent。这种纯基础设施的定位带来两个直接好处一是存储后端可以安全地在进程启动后首次使用时一次性选定并冻结见下文后端选择二是所有安全敏感的加解密逻辑被收敛在少数几个文件内便于审计。模块入口 mod.rs 只负责文档化与重导出真正的实现分布在ops.rs、store.rs、backend.rs、encrypted_file_backend.rs、encrypted_store.rs、file_store.rs、crypto.rs与error.rs中职责划分见下表。文件职责mod.rs模块文档、后端选择优先级说明、公共 API 重导出ops.rs核心操作get/set/delete/is_available/get_or_create_random/migrate_from_fileMigrationOutcome枚举命名空间键助手可用性探测缓存store.rs后端选择与全局状态WORKSPACE_DIR/BACKEND两个OnceLockinit_workspacebackend()build_backend()工作区目录推导backend.rsKeyringBackendtrait、OsBackend系统钥匙串、FileBackend明文 JSON、测试用MockBackendencrypted_file_backend.rsEncryptedFileBackend全部密钥收敛于单个 ChaCha20-Poly1305 加密文件secrets.encencrypted_store.rsSecretStore配置字段加密enc2:/ 遗留enc:主密钥管理与 Windows ACL 修复file_store.rs两个文件后端共享的底层原语跨进程写锁、原子写、损坏文件隔离crypto.rs共享的 ChaCha20-Poly1305 加解密、随机字节、hex 编解码error.rsKeyringError错误枚举与日志安全诊断diagnostic()tests.rs/store_tests.rs/encrypted_store_tests.rs模块测试与测试隔离回归测试二、公共 API命名空间化的密钥读写原语keyring 域对外重导出的核心函数见 mod.rs构成一个简洁的命名空间 用户作用域接口get(user_id, key) - ResultOptionString, KeyringError set(user_id, key, value) - Result(), KeyringError delete(user_id, key) - Result(), KeyringError get_or_create_random(user_id, key, len_bytes) - ResultString, KeyringError is_available() - bool migrate_from_file(user_id, key, path) - ResultMigrationOutcome, KeyringError2.1 多用户命名空间{user_id}:{logical_key}所有后端存储的条目键统一为{user_id}:{logical_key}ops.rs 中的namespaced_key助手。这样设计保证了同一个 OpenHuman 实例上多个用户并存时互不碰撞用户 A 的密钥永远不会被用户 B 读到或覆盖。调用方无需关心某个键最终落到了 OS 钥匙串的哪个账户还是加密文件的哪个 JSON 字段只需传入自己的user_id与逻辑键名。2.2 常用操作的语义get在条目不存在时返回Ok(None)而非报错set幂等地覆盖同用户同名条目delete是幂等的——条目不存在也返回Ok(())get_or_create_random用于要么取回已有密钥要么生成并落盘新密钥的场景如 SecretStore 主密钥若条目已存在则原样返回否则用操作系统 CSPRNGOsRng生成len_bytes字节并写出小写 hex 字符串写入后立即回读校验校验失败返回KeyringError::VerifyFailed。len_bytes必须大于 0否则直接报错。从源码看所有操作的日志只记录user_id与逻辑键名绝不记录密钥值本身log::debug!([keyring] get user_id{user_id} key{key})这是日志侧防泄漏的第一道防线。三、后端选择机制一次选定进程生命周期内冻结这是 keyring 域最值得理解的设计。后端在首次使用时通过store.rs的build_backend_at()一次性选定并存入OnceLockBoxdyn KeyringBackendBACKEND静态量此后整个进程生命周期内不可更改。由于一个进程服务于一个工作区这个OnceLock本质上是纯缓存。3.1 选择优先级README 与 store.rs 中build_backend_at()的实现共同确认了如下优先级环境变量OPENHUMAN_KEYRING_BACKEND显式覆盖取值os、file、encrypted_file大小写不敏感、自动 trim未知值会打印 warn 后落入默认逻辑cfg!(test)测试构建强制file后端保证确定性隔离OPENHUMAN_APP_ENVstaging/production选encrypted_file主密钥存于 OS 钥匙串开发环境dev 默认选file明文后端避免开发构建频繁触发系统钥匙串授权弹窗同时避免 codesign 提示。简言之OPENHUMAN_KEYRING_BACKEND→cfg(test)→ staging/prod 走encrypted_filedev 走file。3.2 四种后端形态后端存储介质用途安全性os系统钥匙串macOS Keychain / Windows Credential Manager / Linux Secret Service服务名openhuman生产默认交给操作系统保管密钥不出系统凭据库encrypted_file{workspace}/secrets.encChaCha20-Poly1305 单文件加密staging / production或显式OPENHUMAN_KEYRING_BACKENDencrypted_file文件本身为密文主密钥在 OS 钥匙串Unix 下权限0600file{workspace}/dev-keychain.json明文 JSONdev 默认、cfg(test)、显式覆盖不加密仅限测试/调试mock进程内HashMap测试专用内存态不落盘从源码看OsBackendbackend.rs通过keyringcrate 以Entry::new(openhuman, namespaced_key)读写系统凭据库并把NoEntry与NoStorageAccess归一化为Ok(None)/ 幂等删除其余错误包装为KeyringError::Os。FileBackend则明确在文档中警告NOT FOR PRODUCTION——这个文件不加密只为了让单元测试与显式调试覆盖独立于宿主机钥匙串。3.3 工作区目录的解析规则文件型后端需要确定dev-keychain.json/secrets.enc的落盘目录规则为见store.rs的resolve_workspace_dir_from_process_state()启动时通过init_workspace()注册的目录WORKSPACE_DIROnceLock否则取环境变量OPENHUMAN_WORKSPACE非空时否则落到~/.openhuman当OPENHUMAN_APP_ENVstaging时落到~/.openhuman-staging。init_workspace幂等——重复调用只打 debug 日志并忽略。3.4 主密钥的懒加载init_master_key对于encrypted_file后端应用级主密钥通过init_master_key()encrypted_file_backend.rs在核心启动时从 OS 钥匙串加载一次槽位openhuman/app:master_key并缓存于进程级OnceLock。这一设计把每个密钥条目各弹一次钥匙串授权的 N 次弹窗问题压缩为每进程一次钥匙串调用——对 dev-signed 的 macOS 构建尤为关键。load_or_mint_master_key()的语义值得特别强调README 记为 #3311 的修复只有遇到真正的NoEntry密钥不存在才允许生成并写入新主密钥凡是访问被拒绝 / 钥匙串锁定 / 平台错误等情形一律返回错误且绝不 mint 新密钥。原因在于 macOS 应用更新可能改变二进制的 code-signing 身份或钥匙串条目的 ACL 信任导致读取既有主密钥时报访问错误而非条目不存在。旧实现把两者混为一谈在访问被拒时生成新密钥结果把旧密钥加密的所有秘密全部孤儿化——表现为 API Key 静默失效、连接器全部断开且毫无警告。现在失败安全fail-safe密文保持原样待下次启动钥匙串访问恢复后即可继续解密。主密钥加载失败时还会通过keyring_consent::policy::notify_master_key_unavailable通知前端而不是静默重置。四、SecretStore配置字段级加密与遗留格式迁移除了整个密钥文件的加密OpenHuman 还在配置字段层面提供加密能力由SecretStoreencrypted_store.rs承担。它解决的是API Key / Token 明文躺在 config 文件里的问题防御目标包括配置文件明文暴露、grep/git log泄密、误提交原始 API Key、已知明文攻击旧 XOR 密码的弱点以及密文篡改认证加密。4.1 密文格式enc2:hex(nonce ‖ ciphertext ‖ tag)SecretStore::encrypt的产物是enc2:前缀 十六进制 blobblob 结构为12 字节随机 nonce ‖ ChaCha20-Poly1305 密文含 16 字节 Poly1305 认证标签。每次加密都生成全新的 12 字节随机 nonce因此同一明文在不同时刻产出的密文不同认证标签保证任何篡改都会在解密时报错。密钥为 32 字节256 位。若用户主动关闭加密secrets.encrypt falseencrypt原样返回明文。解密侧自动识别三种形态decryptenc2:→ ChaCha20-Poly1305当前安全格式enc:→ 遗留重复密钥 XOR 密码仅向后兼容每次走该路径都会打出一条响亮的warn安全警告无前缀 → 视为明文原样返回。4.2enc:→enc2:的自动迁移遗留enc:格式是未认证的重复密钥 XOR存在已知明文攻击面——攻击者只要猜出部分明文例如xoxb-、sk-等 Token 前缀就能通过key[i] ct[i] XOR pt[i]反推主密钥。因此 OpenHuman 在配置加载路径上提供decrypt_and_migrate()读入enc:值时先解密再立即用enc2:重新加密并把新值持久化回配置确保不安全的旧密文不再滞留在磁盘上needs_migration()与is_secure_encrypted()分别用于判断是否需要迁移、是否为安全格式。4.3 主密钥的管理与缓存SecretStore的主密钥在正常构建下存放于 OS 钥匙串槽位secretstore.master_key并通过migrate_from_file完成从遗留{data_dir}/openhuman/.secret_key文件的一次性迁移。密钥一旦解码会以规范化路径为键缓存到进程级缓存cached_key使后续每次解密例如快照轮询直接命中内存。密钥字节使用Zeroizing包装——任何副本返回值、缓存项、中间缓冲在 drop 时都会被清零避免残留在堆、交换区或 core dump 中。Windows 平台上有两处值得注意的工程细节读取重试AV 扫描器如 Defender在文件刚创建后会短暂持有读句柄表现为ERROR_SHARING_VIOLATIONraw OS error 32或PermissionDeniedread_key_file_with_retry以 10ms、20ms、40ms、80ms 的退避最多重试 5 次ACL 自修复若密钥文件因历史icacls误操作失去继承 ACE 而永久不可读repair_windows_acl会依次执行icacls /reset恢复继承与icacls /grant:r DOMAIN\USER:F显式授权并返回文件是否真正可读。五、文件后端的并发安全跨进程锁与原子写README 明确指出两个文件后端都把全部密钥保存在单个文件中因此任意一次set都是读全量 → 改一条 → 写全量的读改写循环。这种形态在单进程内靠 mutex 即可但在 OpenHuman 的真实运行形态下不够——桌面核心、内嵌同一核心的medullaTUI、以及继承了OPENHUMAN_WORKSPACE的cargo test进程可能同时访问同一路径。file_store.rs用两个互补原语封堵两类灾难性故障丢失更新lost updateA 用它在 t-1 读到的旧 map 在 t0 写入静默丢弃 B 在 t-0.5 的写入。对会话密钥来说症状就是刚登录又被登出。解法是lock_for_write()——在旁路文件path.lock上取跨进程建议性排他锁Unixflock/ WindowsLockFileEx对线程与进程统一串行化必须覆盖读与写的完整周期只包住写会重新引入丢失更新。临时文件交错interleaved temp file旧实现用固定的旁路临时路径dev-keychain.json.tmp/secrets.enc.tmp暂存写入两个写者会把半成品 buffer 互相 rename 到最终位置解析成垃圾数据。解法是write_atomic()临时文件名包含进程 PID 单调递增序号TEMP_COUNTER用create_new(true)抢占写完sync_all()后再 rename 覆盖Unix 下在写入任何字节前先设置0600权限避免世界可读窗口。rename 失败时清理临时文件不留下残骸。锁文件为什么不直接锁在密钥文件上因为write_atomic用 rename 替换文件——锁在旧 inode 上等于没锁。get路径无需加锁rename 发布保证读者要么看到完整的旧文件、要么看到完整的新文件绝不看到混合内容。5.1 损坏文件的两种处理策略读降级、写隔离FileBackend::read_map(for_write)对文件解析失败给出两种不可互换的答案读路径for_writefalse降级为空 map调用方看到无此密钥——对会话 Token 意味着重新登录可恢复比让所有无关查询全部失败更好写路径for_writetrue调用quarantine_corrupt把损坏文件改名为dev-keychain.json.json.corrupt.时间戳后返回错误。之所以不能返回空 map是因为写路径返回空正是把损坏文件变成清空全库的元凶——后续那次写会把一张只有待写入键的 map 持久化下来。EncryptedFileBackend同理解密失败或解密结果不是合法 JSON 时把secrets.enc隔离为secrets.enc.enc.corrupt.时间戳并视为空而不是崩溃或覆盖。encrypted_file后端在get时也持有写锁因为read_map可能触发遗留文件迁移或损坏隔离这类文件系统写操作锁可以防止迟到的隔离 rename 覆盖并发set刚发布的文件。六、可用性探测is_available与缓存语义is_available()回答的问题是当前激活的后端在当前机器上是否可用而不是密钥是否在 OS 钥匙串里。其实现ops.rs对os后端做一次真实的写读删往返探测使用保留命名空间__probe__/__openhuman_keyring_probe__先删除可能残留的探测键否则set会因 macOS 的 item already exists-25299失败导致每次启动后探测都误判为 false再set→get→delete并比对回读值。file、mock、encrypted_file后端直接短路返回true。探测结果在AVAILABILITY_CACHE: MutexOptionbool中缓存整个进程只探测一次。选择单个MutexOptionbool而非AtomicBool RwLock是为了消除竞态第一个线程在锁内完成探测并写入结果其余线程阻塞到结果就绪后在同一把锁上读取杜绝标记已探测但结果未写入导致误读false。缓存的意义在于钱包守卫与快照循环这类高频轮询者不必反复触发 OS 钥匙串往返以及 macOS 的访问授权弹窗。当用户在设置中重新授予钥匙串权限后reset_availability_cache()可清空缓存触发重新探测。一个重要的派生结论README 记为 issue #6076 的教训文件后端短路返回true意味着可用性对密钥存储位置没有任何说明。曾有过线上 bug——staging/production 应用在{workspace}/secrets.enc加密文件中存密钥却向用户宣称密钥在 OS 钥匙串中。因此 keyring_consent/policy.rs 的active_mode_for()一律以backend_name()为事实来源推导active_modeos后端 探测通过 →OsKeyringencrypted_file→LocalEncryptedFilefile/mock→LocalPlaintextFile未识别后端宁可报告ConsentPending也不猜测。探测失败会以warn级别记录因为它会静默地把use_keychain翻转为关闭。七、密钥迁移机制migrate_from_filemigrate_from_file(user_id, key, path)是明文文件 → 激活后端的迁移通道其语义由MigrationOutcome枚举表达情形结果后端中已存在该键AlreadyMigrated不做任何事后端无该键但源文件存在读取 → 写入后端 → 回读校验 →校验通过后才删除源文件返回MigratedAndDeleted源文件不存在NoSourceFile实现要点ops.rs六步严格有序——查重、查源文件、读文件trim 后作为值、写后端、回读比对失败返回VerifyFailed、删源文件失败返回MigrationDeleteFailed。任何在文件已读但未删阶段发生的失败都不会删除源文件因此整个迁移是可重试的。这套机制被两处复用SecretStore主密钥从遗留.secret_key迁移进钥匙串EncryptedFileBackend首次读取缺失的secrets.enc时会把遗留明文dev-keychain.json迁移为加密文件并把旧文件改名为dev-keychain.json.migratedrename 失败只告警不阻断迁移本身。八、测试隔离按线程而非按进程解析工作区store_tests.rs记录了一组典型的测试隔离回归测试构建忽略OPENHUMAN_WORKSPACE生产构建仍然尊重它作用域工作区不共享密钥被删除的作用域工作区不能重置默认存储。背景问题在于WORKSPACE_DIROnceLock与OPENHUMAN_WORKSPACE进程级环境变量被同一测试二进制内所有并发运行的测试共享。若测试构建也读取它们整个测试二进制会钉死在首个 keyring 调用恰好观察到的那个工作区当这个赢家是某个测试环境守卫持有的TempDir时该目录在测试结束时被删除而FileBackend::read_map把缺失文件当作空 map——于是下一次写入静默重置了整个存储无关测试读回自己刚写入的密钥得到None。因此cfg(test)下workspace_dir_for_file_backend()完全忽略进程全局状态改用test_scope::current_workspace()测试可用test_scope::ScopedWorkspace绑定线程局部覆盖否则回落到系统临时目录下的稳定按进程目录。后端随后按解析出的目录分别缓存。两个直接后果测试运行永远不读不写开发者真实的~/.openhuman/dev-keychain.json想要私有凭据存储的测试必须显式使用ScopedWorkspace——设置OPENHUMAN_WORKSPACE不再影响测试中的 keyring。生产环境的选择逻辑不受影响。force_backend_for_testpub(crate)仅测试可强制注入自定义后端但若BACKEND已被初始化则 panic——它必须在同一进程内任何 keyring 调用之前运行专用测试二进制或测试最顶端。8.1 历史泄漏清理在隔离修复之前遗留的脏数据可用node scripts/prune-dev-keychain.mjs清理默认 dry-run 报告以已删除TempDir基线命名的dev-keychain.json条目加--apply时先备份再删除。九、错误处理可诊断且可安全记录KeyringErrorerror.rs基于thiserror覆盖以下变体Os底层keyring::Error、InvalidUtf8、MigrationReadFailed、VerifyFailed、MigrationDeleteFailed、RandomGeneration、Crypto、Backend。其中diagnostic()专门解决 macOS 日志可诊断性难题keyring::Error的Display会把错误塌缩成 No matching entry found in secure storage 之类的字符串掩盖了变体与OSStatus让人无法区分钥匙串被锁与授权弹窗被拒。diagnostic()返回Debug形式保留NoEntry/PlatformFailure/NoStorageAccess等变体及其 boxed source 链PlatformFailure携带 security-framework 的OSStatus。同时它可安全写入日志keyring 错误只携带命名空间化的键名从不携带密钥值。十、依赖边界与消费方从依赖关系看keyring 是一个严格的叶子模块内部不依赖其他 openhuman/core 模块只用crate::openhuman::security::keyring::*自引用外部 crate 仅keyring、chacha20poly1305、serde_json、parking_lot、thiserror、anyhow、chrono、dirs外加fs2、zeroize等文件锁/清零依赖。这种低耦合让它可以被安全地嵌入桌面核心、TUI 核心乃至测试进程。README 中列出的消费方与源码相互印证src/lib.rs 与 src/core/jsonrpc.rs启动时调用init_master_key()src/openhuman/security/secrets.rs、src/openhuman/security/mod.rs秘密值处理src/openhuman/config/schema/load.rs配置加载时用SecretStore::new/is_encrypted加解密配置字段src/openhuman/security/credentials/profiles.rs 与credentials/ops.rs按 profile 存储凭据src/openhuman/web3/wallet/ops.rs钱包助记词的is_available/get/setsrc/openhuman/security/devices/rpc.rs设备密钥处理。十一、运维与排障速查环境变量一览环境变量取值作用OPENHUMAN_KEYRING_BACKENDos/file/encrypted_file未知值忽略并告警显式选择后端优先级最高进程内首用即冻结OPENHUMAN_APP_ENVstaging/production其余视为 devstaging/prod 默认走encrypted_filedev 走filestaging 工作区为~/.openhuman-stagingOPENHUMAN_WORKSPACE任意目录路径覆盖工作区目录测试构建下对 keyring 无效secrets.encrypt配置false可关闭字段加密主权用户可要求明文存储常见排障场景密钥文件损坏读路径降级为空表现为重新登录写路径隔离为*.corrupt.ts并报错绝不覆盖主密钥丢失风险绝不因访问被拒而 mint 新密钥#3311 语义恢复钥匙串访问即可复原macOS 弹窗频繁is_available只探测一次并缓存encrypted_file后端整个进程只访问一次钥匙串测试与生产隔离测试按线程解析工作区永不触碰真实~/.openhuman历史脏数据node scripts/prune-dev-keychain.mjs --apply清理遗留TempDir键条目默认 dry-run 并先备份。十二、设计要点小结从该域的 README 与源码可以提炼出几条贯穿始终的设计原则收敛与叶子化所有密钥存储逻辑集中在一个无外部接口的基础设施域便于审计与替换一次选择、永久冻结后端在首用处按env → cfg(test) → 环境 → dev的优先级选定OnceLock保证进程内稳定命名空间化{user_id}:{logical_key}让多用户天然隔离认证加密 纵深防御文件级secrets.enc与字段级enc2:双 ChaCha20-Poly1305 加密遗留 XOR 格式自动迁移且告警失败安全优于静默恢复密钥不可用不 mint、损坏文件隔离不覆盖、迁移校验不过不删源文件跨进程正确性锁覆盖完整读改写周期、原子写用 PID序号唯一临时文件、读降级与写隔离分离可诊断性错误保留底层变体与OSStatus日志只含键名不含值。对于任何需要在自己项目中实现系统钥匙串 加密文件 明文调试 内存模拟四态可插拔密钥存储、且要兼顾多用户隔离、跨进程并发与无损迁移的开发者OpenHuman 的 keyring 域是一份结构清晰、边界分明的参考实现更完整的模块说明可回到 keyring/README.md 继续查阅。【免费下载链接】openhumanOpenHuman is an open source personal AI for Mac, Windows and Linux — local-first memory, agent orchestration, and deep research.项目地址: https://gitcode.com/GitHub_Trending/op/openhuman创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考