我们每天都在用剪贴板搬运密码、密钥和临时链接,却很少有人追问:那个帮我们"接力"的服务器,真的只能看见密文吗?

一个被忽视的默认设置

打开网易新闻 查看精彩图片

大多数剪贴板同步工具有个安静的设计缺陷——服务器能读取所有内容,而大多数人从未察觉。

这款名为"回声"的跨设备剪贴板同步项目,核心诉求很简单:在一台设备复制,在另一台粘贴。难点不在于传输文本本身,而在于传输时不能让服务器学会如何阅读它。

这个项目建立在"盲中继"架构上:Tauri 坐镇原生端,Rust/Axum 后端处理认证与投递,而后端可以在不成为信任模型一部分的前提下移动数据。

通俗地说,盲中继像一名快递员。它能查验身份、运送包裹、送达指定地点。但它打不开包裹。在这个系统里,包裹是密文,只有设备持有密钥

作者选择这条路线,是因为不想接受常见的妥协。不是"传输中加密"却让明文停在中间;不是"静态加密"却让后端仍能检查它中继的内容。在"回声"的设计中,"盲中继"意味着服务器可以认证用户、执行速率限制、存储密文、发送到正确设备、在需要时唤醒休眠客户端。但它没有解密路径。

这听起来抽象,直到你看清剪贴板上实际会出现什么:带临时凭证的 SSH 命令、从内部工具复制的客户数据、嵌入了令牌的管理员 URL、一次性验证码、写了一半的生产环境查询语句。剪贴板充满敏感数据,正因为它们生命周期短、容易被随意对待。

作者第一次强烈感受到这一点,并非在某次安全审计中。而是在把一条短命命令在两台机器间搬运时,发现最快的路径仍是聊天窗口。这就是典型的失效模式——不是戏剧性的泄露,而是一个便利工具继承了它本不该看见的数据。

技术实现:让服务器"失明"的四个关键选择

该项目的描述简单,实践严格。它在一台设备监听剪贴板,用 XChaCha20-Poly1305 在客户端加密载荷,通过 Axum WebSocket 中继发送密文和随机数,仅在接收端解密。

关键选择有四:类型化的 WebSocket 协议、单所有者 WebSocket 接收端、有界队列、客户端加密。结果是密文中继,而非服务端加密的故事。这一区分正是本文的核心。

一旦确定服务器必须是盲的,架构反而变得更简单。客户端拥有密钥。客户端在网络传输前加密。后端处理投递、排序和背压。Rust 成为合适的语言,因为难点在于所有权、协议设计、并发,以及在坏状态扩散前消除它们。

架构拆分:两个世界之间的硬边界

该系统有两个截然不同的部分,以及它们之间的一条硬边界。

后端是纯 Rust:Axum 处理 HTTP 和 WebSocket,Tokio 调度并发,Redis 存储短暂状态(设备在线状态、待投递消息)。没有数据库,没有长期存储。后端只关心谁能接收什么、何时接收,以及以什么顺序接收。它不触碰明文。

客户端是 Tauri:Rust 核心处理加密和协议,Web 前端处理 UI。Tauri 让作者能用 Web 技术构建界面,同时让加密代码在原生层运行,远离浏览器沙箱的边界。

硬边界在 WebSocket 协议层。两端都使用强类型消息,但后端只验证、路由、存储(短暂地)、转发。它从不反序列化载荷字段。那个字段对后端是不透明字节。

这种分离不是装饰性的。它让作者能在不重新审计加密路径的前提下更改后端。它也意味着即使后端被攻破,攻击者获得的只是密文块和元数据(谁发给谁、何时、多大)。

协议设计:类型安全作为防御层

WebSocket 协议是作者做过最谨慎的设计之一。每条消息是一个枚举变体,携带特定载荷。后端匹配变体,提取路由信息,从不触碰内部。

例如,剪贴板同步消息的结构大致如下:

设备发送 `ClipboardSync { target_device_id, encrypted_payload, nonce }`。后端验证发送者拥有 `target_device_id`,检查速率限制,将密文存入目标设备的队列,如果目标在线则通过 WebSocket 推送。

后端从不解析 `encrypted_payload`。它在代码中是 `Vec`。Serde 反序列化跳过它。即使有人向服务器发送恶意构造的载荷,服务器也只能存储和转发,无法提取内容。

这种设计选择让协议成为防御层。类型系统保证后端代码无法意外访问明文,因为明文在编译期就不可达。