OpenHuman 移动设备配对安全域全解析:X25519 密钥协商与 XChaCha20-Poly1305 端到端加密隧道实战指南
OpenHuman 移动设备配对安全域全解析X25519 密钥协商与 XChaCha20-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/openhumanOpenHuman 的devices安全域src/openhuman/security/devices负责在 Rust 核心与 iOS 客户端之间搭建一条端到端加密隧道通过 tinyhumans 后端的tunnel:*Socket.IO 中继完成设备配对、密钥协商与加密帧转发。本文以该域的 README.md 为主线结合源码逐层拆解配对通道注册、X25519 密钥协商、方向性会话密钥派生、帧加密与重放保护、SQLite 持久化及事件驱动集成帮助你完整掌握这一扫码即配、重启可恢复、帧帧加密的移动设备互联实现方案。域定位与核心职责devices是 OpenHuman 的移动设备配对mobile-device pairing领域模块。它承担一个核心职责在 Rust 核心与 iOS 客户端之间经由 tinyhumans 后端的tunnel:*Socket.IO 中继搭建一条安全的端到端加密隧道。配对完成后帧的机密性与完整性由 XChaCha20-Poly1305 提供密钥材料来自 X25519 协商出的共享秘密。从命名上看它是 iOS 端TunnelTransport策略在 Rust 侧的对等实现。按 README 的归纳该域承担如下职责向后端隧道注册新的配对通道tunnel:register返回二维码所需字段channel_id、pairing_token、core_pubkey、可选的rpc_url与expires_at每次配对生成一把 X25519 静态密钥对私钥经SecretStore加密后持久化保证重启后重连握手仍可恢复密钥以role:core身份在通道上执行tunnel:connect开始监听设备接入当设备发来首个tunnel:frame时完成 X25519 握手sealed-handshake 或明文公钥回退两种格式派生共享秘密并持久化一条PairedDevice记录跟踪来自tunnel:peer-status的实时在线状态并将其叠加到devices_list的返回结果上列出未撤销设备撤销设备软删除并清理其内存态与隧道态提供可复用的TunnelCipher带 128 条重放保护窗口的 seal/open用于隧道帧加解密尽力探测局域网内可用的rpc_url为直连 HTTP 快速路径提供地址。值得注意的是该域没有任何 Agent 工具无tools.rs它的能力全部通过 RPC 控制器与事件总线对外暴露。目录结构与模块划分文件角色src/openhuman/security/devices/mod.rs纯导出层模块文档、pub mod声明以及 schema 注册函数与公开类型的 re-exportsrc/openhuman/security/devices/types.rsSerde 领域类型PairedDevice、PairingSession及三个 RPC 响应载荷src/openhuman/security/devices/rpc.rs三个 RPC 方法处理器 模块级内存态单例PENDING_KEYPAIRS、PERSISTED_KEYPAIRS、PENDING_SESSIONS、PEER_STATUS、ACTIVE_CIPHERS、LAN 地址探测、SecretStore密钥持久化/恢复src/openhuman/security/devices/schemas.rs控制器 schema、all_controller_schemas/all_registered_controllers以及委托给rpc.rs的handle_*桥接函数与cron/schemas.rs同构src/openhuman/security/devices/store.rsSQLite 持久化paired_devices表采用与cron/store.rs相同的每次调用with_connection模式src/openhuman/security/devices/crypto.rsDeviceKeypairX25519 生成/DH/字节往返、TunnelCipherXChaCha20-Poly1305 seal/openWINDOW_SIZE128重放窗口、base64url 辅助函数、derive_session_keysHKDF-SHA256 方向性子密钥src/openhuman/security/devices/tunnel_client.rs通过共享的SocketManager发出/解析tunnel:*事件线协议类型tunnel:register经SocketManager::emit_with_ack走 Socket.IO ACK帧上限 64 KBsrc/openhuman/security/devices/bus.rsDeviceTunnelSubscriber事件处理器——驱动握手完成、持久化与在线状态更新公开面从mod.rsre-exportall_devices_controller_schemas/all_devices_registered_controllers即schemas::all_controller_schemas/all_registered_controllers的别名类型CreatePairingResponse、ListDevicesResponse、PairedDevice、PairingSession、RevokeDeviceResponse。其他跨 crate 使用但不在此域根 re-export 的重要公开项bus::register_device_tunnel_subscriber、crypto::{DeviceKeypair, TunnelCipher, base64url_encode, base64url_decode}、tunnel_client::{emit_register, emit_connect, emit_frame, TunnelPeerStatus, TunnelFrame, TunnelRegisterResponse}。密钥材料与帧密码学实现DeviceKeypair每次配对的静态 X25519 密钥对src/openhuman/security/devices/crypto.rs 中的DeviceKeypair是核心持有的静态设备配对密钥。generate()使用rand::random::[u8; 32]()生成 32 字节随机私钥经StaticSecret::from构造 X25519 私钥再由PublicKey::from(private)导出公钥最终以 base64url 编码存储于pubkey_b64该值即二维码载荷中的core_pubkey。关键方法derive_shared_secret(peer_pubkey_b64)解码并校验对端公钥必须恰好为 32 字节执行diffie_hellman得到 32 字节共享秘密private_bytes()导出私钥字节供加密持久化from_private_bytes(bytes)从解密后的私钥字节重建完整密钥对——这是重启后恢复配对身份的基础。会话密钥派生静态 DH 临时 DH 的 HKDF 组合配对升级后帧版本0x02会话密钥不再由单一共享秘密直接充当而是经过derive_session_keys用 HKDF-SHA256 派生两个方向独立的子密钥ikm static_dh(32) || eph_dh(32) salt client_eph_pub || server_eph_pub c2s HKDF-SHA256(ikm, salt, info openhuman-tunnel/v1/c2s, 32) s2c HKDF-SHA256(ikm, salt, info openhuman-tunnel/v1/s2c, 32)设计意图源码注释明确static DH 用于认证对端它是基于扫码二维码来源的长期配对密钥协商结果eph DH临时密钥提供前向保密会话开始时双方各自铸出全新的临时密钥对即使日后静态密钥泄露历史会话流量也无法被恢复salt 固定为client_eph_pub || server_eph_pub顺序把派生密钥同时绑定到临时交换的两半任一方都无法单方面固定 saltHKDF 的info标签是钉死的字节串HKDF_INFO_C2S/HKDF_INFO_S2C版本前缀v1对端实现必须使用完全一致的取值版本化标签使得未来 KDF 标签变更无需扰动FRAME_VERSION计数器。TunnelRole枚举Client/Server决定每个对端如何使用这两个子密钥客户端用c2s封、s2c开服务端桌面核心反之。由于每个对端各自持有一把seal_cipher本地方向与open_cipher对端方向服务端发出的帧永远不会在自己的解密器里被解密成功从而封死了跨方向反射重放这一类攻击。TunnelCipher帧格式、版本与重放保护TunnelCipher是一个有状态的帧密码器其帧格式为version(10x02) || nonce(24) || ciphertexttagseal随机生成 24 字节 XChaCha20-Poly1305 nonce加密后拼接版本字节与非密文openmut self——依次拒绝空帧、version0x01的旧帧返回明确的UnsupportedFrameVersion提示对端需重新配对以升级到 v2 方向性子密钥、版本不匹配帧、长度不足以容纳 nonce 的帧然后做重放检查与 AEAD 认证解密。重放保护采用滑动窗口维护最近WINDOW_SIZE 128条已见 nonce 的VecDeque新 nonce 若已存在则拒绝窗口满时弹出最旧条目。源码注释特别提醒由于open是mut self调用方必须用Mutex/RwLock包裹后使用——实际代码中ACTIVE_CIPHERS单例正是以ArcMutexTunnelCipher存放。TunnelCipher::new(key)是遗留的单密钥构造方式seal/open 共用同一把钥匙仅在bus.rs的 Layer-2 sealed-handshake 确认路径中用于握手 ACK 的引导加密会话建立后的正式流量必须走for_role(role, keys)方向性构造。帧版本升级的兼容性策略FRAME_VERSION常量当前为0x02LEGACY_FRAME_VERSION_V1 0x01。v1 帧单共享密钥、双向同钥、无 KDF在升级后不再被接受对端必须重新配对。由于 iOS 客户端在CLAUDE.md中被标记为 in-progress / 未发布强制重配对是可接受的代价——这是该项目在当前阶段采取的一次性协议升级策略。配对流程实战三个 RPC 方法devices域注册在控制器注册表中src/core/all.rs 的DomainGroup::Security分组命名空间为devices调用方式为openhuman.devices_function。schema 定义见 src/openhuman/security/devices/schemas.rs处理器桥接函数经load_config_with_timeout加载配置后委托给 rpc.rs。方法输入输出行为devices_create_pairinglabel?: stringCreatePairingResponse经 Socket.IO ACK 注册通道、生成并持久化密钥对、发出无 token 的 core 端tunnel:connect返回使用后端配对过期时间的二维码字段devices_list—ListDevicesResponse列出未撤销设备并从PEER_STATUS叠加实时peer_onlinedevices_revokechannel_id: stringRevokeDeviceResponse软删除设备、清空该通道全部内存态、发布DeviceRevokeddevices_create_pairing从注册到二维码src/openhuman/security/devices/rpc.rs 的实现分为五个步骤调用tunnel_client::emit_register()向后端发出tunnel:register后端经 Socket.IO ACK 返回{channelId, pairingToken, pairingExpiresAt}DeviceKeypair::generate()生成 X25519 密钥对把私钥经SecretStore::encrypt加密后以enc2:字符串形式写入PERSISTED_KEYPAIRS单例键为channel_id把密钥对Arc化存入PENDING_KEYPAIRS供bus.rs在握手时克隆使用克隆而不持锁执行 DH避免长时间锁竞争尽力探测 LANrpc_url非致命失败仅记日志先插入PENDING_SESSIONS再执行tunnel:connect——因为emit_connect之后bus.rs就开始处理tunnel:frame且握手持久化要从该 map 派生配对凭据先插入可避免快速入站帧与条目写入之间的竞态源码注释援引 CodeRabbit #4355。tunnel:register的 ACK 解析逻辑tunnel_client.rs值得单独说明后端失败时返回{ ok: false, error: ... }信封若把它按成功形态解析会得到missing field channelId并把后端给出的真实失败原因丢掉源码注释将此记为 #5871 的教训。因此解析顺序是先检查失败信封backend_ack_error判定ok字段不等于true即视为拒绝且非字符串error也会被渲染保留再解析成功形态解析失败时日志会描述 ACK 实际形态对象字段名、数组、字符串等便于排查。同时pairingExpiresAt经自定义反序列化器expires_at_from_string_or_epoch_millis兼容 ISO 8601 字符串与 epoch 毫秒两种形态并对小于 2020-01-01 的看似秒级数值直接报错避免秒值静默解码成 1970 年导致二维码刚生成就已过期的迷惑性故障。握手完成与设备持久化设备端首个tunnel:frame到达后事件经平台层重新发布为DomainEvent::DeviceTunnelFrame由 bus.rs 的handle_tunnel_frame处理先解码外层 base64url 信封若首字节为FRAME_VERSION0x02直接走加密帧 RPC 通道见下文隧道内 RPC否则按握手帧处理0x01为 sealed-handshake其余可打印 ASCII 回退为明文 base64url 设备公钥兼容 Layer-2 之前的旧设备。Sealed-handshake 帧格式0x01 || eph_pub(32) || nonce(24) || ciphertexttag设备生成临时 X25519 密钥对与corePubkey做 DH用得到的密钥把自身静态公钥32 字节以 XChaCha20-Poly1305 密封核心用PENDING_KEYPAIRS中该通道的静态私钥与eph_pub做 DH 得到解密密钥剥掉eph_pub前缀后直接调用XChaCha20Poly1305解密nonce||ct注意此路径不复用TunnelCipher::open因为帧里没有版本字节前缀。解密出的 JSON 若含client_ephemeral_pubkey字段则进入 v2 会话密钥安装流程。v2 会话建立install_v2_cipher_and_ack核心铸出新的临时密钥对与设备临时公钥做 DH 得到eph_dh调用derive_session_keys(static_dh, eph_dh, client_eph_arr, server_eph_pub)派生c2s/s2c以TunnelRole::Server构造方向性TunnelCipher存入ACTIVE_CIPHERS用遗留TunnelCipher::new(static_dh)单密钥引导密封handshake_ack帧含server_ephemeral_pubkey回发给设备完成双向临时密钥交换。随后无论是否启用 v2核心都会执行静态 DHkeypair.derive_shared_secret(device_pubkey_b64)验证设备公钥合法性并从PENDING_SESSIONS读取label与pairing_token缺失会话则 fail-closed 跳过持久化防止把空 token 的哈希写入遗留列以 SHA-256 哈希配对凭据写入遗留列core_session_token_hash最后调用store::insert_device持久化并发布DevicePaired事件。devices_list本地数据 实时状态叠加devices_list先从 SQLite 读取全部未撤销设备WHERE revoked 0 ORDER BY created_at ASC随后遍历PEER_STATUS单例把每个channel_id对应的在线布尔值写入devices[i].peer_online。peer_online不落库——它只存在于内存PEER_STATUS映射中这正是持久层只存配对事实、在线状态实时感知的清晰边界。devices_revoke本地软删除devices_revoke依次执行store::revoke_device将revoked置 1软删除清理PENDING_KEYPAIRS、PENDING_SESSIONS、PEER_STATUS、PERSISTED_KEYPAIRS、ACTIVE_CIPHERS中该通道的全部内存态发布DomainEvent::DeviceRevoked通知 UI 与其他订阅者。需要注意的限制目前尚无后端撤销端点README 标注 TODO指向 PR #709 的后续工作本地只拆除隧道侧状态后端通道依赖配对 token 的 TTL 自然过期。这属于当前实现的已知边界而非缺陷。隧道内 RPC加密信封与请求转发配对完成后tunnel:frame0x02版本承载的是结构化隧道信封bus.rs的handle_encrypted_rpc_frame负责从ACTIVE_CIPHERS取该通道的ArcMutexTunnelCipher持锁open解密反序列化TunnelEnvelope { requestId, kind, seq, payload }仅处理kind request从payload提取TunnelRpcPayload { method, params }调用crate::core::jsonrpc::invoke_method(default_state(), method, params)执行真实 RPC结果成功response或失败error以同构信封封装经cipher.seal加密后emit_frame回发设备。这意味着配对后的移动端可以通过同一加密隧道代理执行核心的 JSON-RPC 方法而不必暴露额外的网络端口这是扫码配对后远程调用桌面核心能力的实现基础。持久化设计SQLitepaired_devices 表数据库位于{workspace_dir}/devices/devices.db建表 DDL 在with_connection打开连接时幂等执行CREATE TABLE IF NOT EXISTS并开启PRAGMA foreign_keys ON。表结构列说明channel_id主键128 位 base32 通道 IDlabel人类可读标签device_pubkeyBase64url 编码的 X25519 设备公钥core_session_token_hash遗留列名因后端不再铸造 core 会话 token现存配对凭据的 SHA-256 哈希shared_secret_encryptedBLOB当前恒写NULLcreated_at/last_seen_atISO 8601last_seen_at由touch_device在tunnel:peer-status上线事件时更新revoked软删除标志list_devices过滤revoked 0with_connection每次调用都打开新连接与cron/store.rs同模式读路径查询映射map_device_row把 SQLite 行还原为PairedDevice其中peer_online恒为None由内存态填充。SecretStore加密私钥的可恢复性配对时生成的 X25519 私钥并不以明文落盘。它先经keyring::SecretStoreChaCha20-Poly1305 加密返回enc2:前缀字符串加密再以channel_id为键存入内存PERSISTED_KEYPAIRS单例。load_keypair_from_storerpc.rs负责恢复从单例取密文 →SecretStore::decrypt→ base64url 解码 → 校验 32 字节长度 →DeviceKeypair::from_private_bytes重建密钥对。重启后bus.rs在重连握手时无需重新生成密钥而是直接恢复原密钥对从而保证配对身份的连续性。需要明确PairingSession与各密钥对映射仅存在于内存TTL/清理语义委托给后端撤销时统一清空SecretStore的作用域是工作区目录config.config_path.parent()。事件驱动集成事件来源与订阅原生tunnel:peer-status/tunnel:frame/tunnel:evictedSocket.IO 事件由 src/openhuman/platform/socket/event_handlers.rs 解析复用本域的TunnelPeerStatus/TunnelFrame线协议类型并重新发布为DomainEventtunnel:peer-status→DevicePeerOnline/DevicePeerOfflinetunnel:frame→DeviceTunnelFrametunnel:evicted后端因 TTL/服务重启逐出通道→DevicePeerOffline。DeviceTunnelSubscribername() device::tunnel、domains() [device]在启动时由 src/core/jsonrpc.rs 通过register_device_tunnel_subscriber注册幂等OnceLock保证只注册一次。其处理逻辑DevicePeerOnline/DevicePeerOffline→ 更新PEER_STATUS不重复发布事件来源已由平台层发布DeviceTunnelFrame→ 完成握手 持久化PairedDevice。发布事件DevicePaired握手成功且持久化完成后发布携带channel_id、device_pubkey、labelDeviceRevoked由devices_revoke发布。值得注意的是 README 的提示本域只消费在线状态事件不负责重新发布 peer-status避免事件环路。依赖关系梳理crate::openhuman::configConfig、config::rpc::load_config_with_timeout——工作区路径与配置加载crate::openhuman::security::keyring::SecretStore——X25519 私钥静态加密crate::openhuman::platform::socket::global_socket_manager——复用共享的后端 Socket.IO 连接发出tunnel:*事件不额外开第二个 WebSocketcrate::core::event_buspublish_global、DomainEvent、EventHandler、SubscriptionHandle、subscribe_global——设备隧道事件的发布/订阅crate::core::allControllerFuture、RegisteredController与crate::core::{ControllerSchema, FieldSchema, TypeSchema}——控制器注册表契约crate::rpc::RpcOutcome——RPC 处理器返回类型外部 craterusqlite、chacha20poly1305、x25519-dalek、base64、sha2、chrono、once_cell、tokio、async_trait、anyhow。LAN 直连快速路径detect_lan_rpc_urlrpc.rs使用经典 UDP 技巧探测本机 IPv4UdpSocket::bind(0.0.0.0:0)后connect(8.8.8.8:80)不真正发包再读local_addr()取得出口网卡的 IPv4 地址排除环回地址。端口取自环境变量OPENHUMAN_CORE_RPC_PORT默认7788最终生成http://{ip}:{port}/rpc形式的 URL 放入配对响应。探测失败是非致命的返回None二维码中rpc_url字段保持省略设备可回退到经后端中继的隧道路径。关键注意点与实战提醒以下是 README Notes / gotchas 与源码共同确认的边界条件对接入方与维护者都有实际价值握手帧格式0x01sealed-handshakeeph_pub(32) || nonce(24) || ciphertexttag设备在临时 DH 下密封其静态公钥非0x01/0x02首字节回退为整包明文 base64url 设备公钥pre-Layer-2 兼容。sealed-handshake 解密路径不复用TunnelCipher::open而是剥掉eph_pub前缀后直接以XChaCha20Poly1305解nonce||ctTunnelCipher帧格式version(1)0x02 || nonce(24) || ciphertexttag每帧随机 nonce128 条滑动窗口防重放open为mut self调用方需在外层加Mutex/RwLocklabel 回退配对持久化时的label当前回退为channel_idPairingSession没有真正的 label 字段以channel_id作为 label 来源传入的label参数仅出现在请求层撤销范围devices_revoke只拆除本地与内存态无后端撤销端点TODO 指向 PR #709 后续后端通道靠配对 token TTL 过期LAN 探测UDP connect 8.8.8.8 技巧只取出口 IPv4端口来自OPENHUMAN_CORE_RPC_PORT默认7788失败不致命register ACKSocketManager::emit_with_ack期望后端 ACK 形态{channelId, pairingToken, pairingExpiresAt}超时 10 秒pairingExpiresAt同时兼容 epoch 毫秒与 ISO 8601内存态生命周期PairingSession与密钥对映射仅存内存TTL/清理委托后端语义撤销时清空帧速率与大小限制出站tunnel:frame载荷上限64 KBtunnel_client::emit_frame在发出前即校验长度调用方应保持 ≤ 100 帧/秒。测试与验证本域为每个实现文件配齐了同目录测试文件可在仓库中直接查阅以深入验证上述行为crypto_tests.rs——密钥派生、帧加解密、重放拒绝、版本拒绝等密码学路径rpc_tests.rs——三个 RPC 方法的输入校验与状态变化schemas_tests.rs——控制器 schema 与注册表一致性store_tests.rs——SQLite 插入/查询/撤销/触达逻辑tunnel_client_tests.rs——ACK 解析、失败信封判定、超长帧拒绝。这些测试与 src/core/all.rs 的控制器注册、src/core/jsonrpc.rs 的订阅注册、src/openhuman/platform/socket/event_handlers.rs 的事件解析共同构成该域的完整闭环从扫码二维码拿到channel_id pairing_token core_pubkey到设备发来首个加密帧完成 X25519 握手再到每次交互都经过 XChaCha20-Poly1305 加密、128 条重放窗口防护、SQLite 持久化与事件通知——这正是 OpenHuman 本地优先、端到端加密的移动设备互联能力的 Rust 侧完整实现。【免费下载链接】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),仅供参考