端到端加密,密钥在你自己的设备上
Happier 让一个编码会话在它运行的那台电脑和你用来查看它的每一台设备之间保持同步,而中间坐着一台 relay 服务器。本页是这句话底下的架构:哪一把密钥在哪里生成、relay 实际拿着什么、它仍然能看到什么,以及在一台被配置为关闭加密的服务器上有什么不同。
一条消息在你的手机和你的电脑之间是如何加密的
你从手机发出的一条消息,画在它经过的三个节点上。智能体回复的一切沿同一条路径反向传回,在离开你的电脑之前就已封装。
- 你的手机
- 在这里封装. 消息在你敲下它的那台设备上加密,用的是一把只属于这个会话、别无他用的密钥 —— 会话创建时抽取的 32 个随机字节,AES-256-GCM,每条消息一个新的 nonce。随后这把会话密钥通过一次临时的 X25519 交换封装给你账号自己的密钥,交给 relay 的是这份封装后的副本。
- relay
- 封装着传递. 服务器写下的是一行形如 { t: "encrypted", c: "…" } 的记录 —— base64,外加一个它没有对应私钥的封装密钥信封。它可以路由这一行、统计它、保存它、再把它交还回去。它没有任何能打开它的东西,也没有任何端点要求它这么做。
- 你的电脑
- 在这里打开. CLI 用你连接终端时拿到的账号密钥打开封装的会话密钥,解密,再把明文交给 Claude Code、Codex、OpenCode 或正在运行的那个智能体。回复沿相反方向走同一趟路,在离开之前就已封装。
relay 服务器存储什么
加密是关于内容的一种主张,而关于内容的主张只是答案的一半。这是另一半,详细程度相同:运营这个 relay 的服务器在没有密钥的情况下能读到哪些列。
- 密文,以及一个封装的密钥信封
- 每条消息一行,只装着 base64,别无其他,外加该会话被封装的数据密钥。封装是在设备上完成的;没有哪个服务器路由会产生一个。
- 标识符和形态
- 账号、设备和会话 id,每个会话一个随机标签,序列号、时间戳和大小。这是任何做同步的系统上加密都覆盖不到的部分,值得直说:relay 知道你发了多少、以及什么时候发的。
- 每条消息来自哪一方
- 每条消息都带一个角色 —— user、agent、event 或 unknown —— 由客户端连同密文一起提供。它是一个列,好让会话记录在不被打开的情况下分页和计数。在加密的行上,服务器无法推导出它;它只能存下客户端报上来的东西。
- 一份有意做小的活动投影
- 一个会话是否未读、有多少权限请求在等待、智能体问了多少个问题、它是否在思考、上一轮是怎么结束的。这里的每一项都是派生状态 —— 一个计数器、一个标志,或一个由客户端在会话推进时写下的时间戳 —— 从来不是说过的话里的任何一个字。它存在,是为了让一台合了一小时的手机在你打开的那一刻显示正确的角标,而计数就是服务器为此被允许做的全部。
- 你输入的内容,以及智能体的回复
- 会话记录就是密文。没有第二份副本,服务端没有针对它的索引,也没有哪个需要索引的摘要步骤。
- 会话运行在哪里
- 项目路径、你的主机名、你的主目录和 CLI 版本都是会话元数据,而元数据在会话创建之前就用同一把会话密钥加密了。它们是封装数据块内部的字段,而不是服务器可以拿来排序的列。
- 你的服务方凭据
- 一个已连接的账号 —— 会话所依据的 Claude 或 ChatGPT 凭据 —— 在上传之前就在设备上封装好,用的是一把从你的账号密钥为这一个用途派生出来的密钥。relay 存下一个信封,并把它交还给你自己的设备。
- 你的仓库
- 你的仓库从不会被同步或镜像。智能体在它运行的那台电脑上读写文件,那台电脑始终是唯一的完整副本。当你索取一个文件、一个文件夹或一份 diff 时,它走的是和其他一切相同的封装路径 —— 因为是你要的,而不是因为 Happier 留了一份副本。
你的加密密钥是在你自己的设备上创建的
你的账号根部有一个密钥,它是在设备上创建的 —— 不是由服务器签发的。其他一切都由一棵密钥树从它派生出来:一个 HMAC-SHA-512 的根,然后每个带标签的路径元素做一次 HMAC。能打开封装会话密钥的那对密钥,出自这棵树上标签为 "content" 的位置,其他每一类存储的数据块也各有一把单独的密钥,所以没有哪把密钥身兼两职。
这就是为什么一个全新的浏览器可以登录你的账号,却仍然看不到上周的任何内容。登录向 relay 证明你是谁。读取旧会话需要密钥,而密钥并不在 relay 上、无从下发。让密钥转移过来的那一步是恢复设备 —— 从一台已经持有它的设备,或者从你自己保存的私钥。
这也意味着丢失的情形是真实存在的,值得说明一次:如果所有已登录的设备都没了,私钥也随之丢失,那么加密的会话无论是我们还是任何人都无法恢复。一个能把它们交还给你的 relay,也就是一个能读它们的 relay。
添加一台新设备,以及你的密钥如何到达它
添加设备是密钥材料发生转移的时刻,所以也是值得弄清楚的时刻。扫描二维码会发起一个请求,它做的就只有这一件事:批准发生在一台已经登录的设备上,当两块屏幕都显示出一个简短的确认码时,你应该先比对再批准。
批准实际做的事,是把内容密钥封装给发起请求的那台设备的公钥,装进一个带有临时发送方密钥和新 nonce 的信封。这个信封和其他一切一样经由 relay 传递。连接终端时还会在此之上再加一道绑定:载荷会针对一个随二维码传递、而非经服务器传递的配对密钥做认证,所以一个替换了载荷的 relay 无法生成匹配的标签。
存储策略:端到端加密是默认设置
上面所有内容描述的都是默认设置,而这个默认不是客户端可以劝服务器改掉的建议。全新的 relay 被设置为要求端到端加密存储:明文写入会以策略错误被拒绝,而不是被悄悄接受。客户端在创建会话之前会从服务器自己的 features 端点读取该策略,所以一个会话处于哪种模式,在它的第一条消息之前就已经谈妥了。
服务器运营方还有另外两个可选设置。一个是允许账号自行选择,于是加密会话和明文会话共存于同一个 relay 上 —— 这需要同时设置可选策略和退出许可,而这个许可在运营方打开之前是关闭的。另一个是要求明文,面向在基础设施层管理加密、并且需要服务器能够读取和处理会话内容的组织。最后这个是一个真实的取舍,值得不加粉饰地说出来:在明文会话上,服务器可以读取会话记录。这正是这个设置的用意。
模式在会话创建时就按会话固定下来,所以一个会话不会一半是这样、一半是那样,而修改一个账号的模式改变的是新会话的走向,不会回头去动旧的。没有任何东西会被追溯解密,也没有任何东西会被追溯加密。
设定这三者的变量、它们底下的静态存储选项,以及围绕它们的身份控制,是运营方那一半的内容,都在面向团队的自托管页面上。
推送通知由你自己的电脑发送
推送通知是唯一一件跳出这个模型的事,因为它必须送达一台什么都没打开的手机。Happier 的答案是:由你自己的电脑来发。服务器保存你的推送令牌;你自己电脑上的 CLI 把它们读回来,自己发送通知,这样 relay 就留在一条它本来必须被告知内容的路径之外。
载荷里带的东西是有意做薄的:工具或请求的类型,以及在有帮助时的一小段提示 —— 比如 src/file.ts 这样的文件、一个命令名、像“3 个问题”这样的计数。工具的原始输入不会放进去。足够让你判断要不要拿起手机,而且是在任何站在你身后的人都能读到的屏幕上。
自托管 relay:元数据也留在你自己的硬件上
以上这一切在一个不由你运行的 relay 上同样成立。自己运行一个的理由,是上面关于标识符和形态的那一行:加密并不掩盖谁在和谁说话、以及多久说一次,而在你自己的 relay 上,那本账也是你的。一条命令就能把 relay 装在你自己的硬件上,路径于是变成你的设备、你的 relay、你的电脑,其中没有一样不是你部署的。
自己去读加密代码
本页没有任何认证,因为确实没有可以报告的认证,而一个暗示自己拥有并不具备的合规资质的安全页面,比只字不提的更糟。这里给出的是上面每一句话所依据的代码,采用 MIT 许可,在你运行之前就可以读:packages/protocol/src/crypto 里的原语、apps/cli/src/api/encryption.ts 里的会话加密、服务器在它的 features 端点上执行的存储策略,以及能确切显示哪些列是明文的数据库 schema。
这也是关于你该如何看待本页的诚实答案:把它当成一张告诉你去哪里看的地图,而不是一份需要凭信任接受的承诺。读那四处代码,或者自己运行 relay,读一读它向客户端公布了什么。
加密模型参考以操作流程的形式讲同一片内容 —— 恢复会要求你做什么、每种存储模式对一个账号意味着什么、当一台设备还读不了某个会话时该走哪个流程。

