cryptofoundry

联系 cryptofoundry

告诉我们您想构建或自动化的内容。

Ecosystem & Integrations

无客户端到服务器通道的推送通知:设计、权衡与已知的注销竞态条件

推送通知是私密通讯软件最容易妥协的地方。必须有人被告知消息已送达,而在 iOS 上,那个人是 Apple。本说明描述了 ADAMANT 如何构建此流程以确保通知服务获取的信息量降至最低、该设计的成本,以及项目决定容忍而非掩盖的一个已知故障模式。

系统架构

涉及四个参与方:用户设备、ADAMANT 节点、Apple 推送通知服务 (APNs) 以及由 cryptofoundry 运营的 ADAMANT 通知服务 (ANS)。

注册过程通过区块链而非 API 进行。设备首先向 APNs 请求推送令牌。随后,应用程序将 {token, provider, action} 加密为 ANS 公钥,并通过用户选择的任意节点,将其作为信号消息(聊天类型 3,AIP-6)发送至 ANS 账户的 ADM 地址。ANS 轮询节点以获取发送给自身的交易,对其解密并存储 ADM 地址 → 推送令牌 的对应关系。

投递过程则是其镜像。ANS 轮询转账(类型 0)和聊天(类型 8)交易,根据其注册表核对接收者,并请求 APNs 发送推送。

各方实际获取的信息

通知负载不包含任何消息内容:

{
  "aps": {
    "alert": { "loc-key": "NotificationsService.NewMessage.BodySingle" },
    "badge": 1,
    "mutable-content": 1,
    "sound": "notification.mp3"
  },
  "push-recipient": "U1234567890123456",
  "txn-id": "7175005690801347553"
}

正文是一个本地化键,而非消息本身。应用程序的通知服务扩展 (Notification Service Extension) 获取 txn-id,从节点拉取交易,并在渲染通知前使用用户的私钥在本地解密。Apple 仅能看到设备令牌、ADM 地址、交易 ID 和时间信息,绝不会看到内容。这是一个值得明确说明的真实披露:如果您向 Apple 推送,Apple 就会知道该地址何时接收了内容。

更有趣的特性在于服务器端。应用程序从不与 ANS 建立连接。iOS 客户端的令牌服务仅发出两种网络调用,且均指向 ADM 节点,应用程序中未出现任何推送服务的域名。因此,ANS 仅能看到链上已公开的信息,且关键在于,它永远无法获取设备 IP 地址。通过自身节点或 Tor 路由的用户完全不会触及 cryptofoundry 的基础设施。

这一特性是设计的核心,且非常脆弱。显而易见的便利功能——在 ANS 上设置一个小型 HTTPS 端点以便客户端询问“你还有我的令牌吗?”——会悄然摧毁这一特性。它会将设备 IP 暴露给 ANS,使每次应用启动都变成针对设备的活跃度信号,破坏用户对节点的选择权,并将原本可插拔的节点列表变成一个单一的可封锁域名。该端点不会被添加。

注册生命周期及其隐患

由于通道是区块链,add(添加)和 remove(移除)均为交易。每条信号消息的成本为常规聊天费用:constants.fees.chat_message = 100000(fixedPoint = 1e8),即 0.001 ADM。这比价格本身更重要——余额为零的用户根本无法发送,这排除了“定期重新注册”作为稳健性策略的可能性。

客户端保留其认为已注册的令牌的本地副本,仅在 iOS 提供不同令牌时才重新注册。它从不询问服务器注册是否仍然存在,因为如果不放弃隐私特性,它就无法进行询问。

这产生了一个已知且可复现的故障模式,且无需服务器端丢失数据。用户在离线或连接到不稳定节点时退出登录;客户端清除其缓存的令牌并发送 remove(T),但发送失败,因此该交易被持久化并在后续每次应用启动时重试。随后,用户在同一设备上重新登录。iOS 提供相同的令牌 T。本地缓存为空,因此客户端发送 add(T) 并成功——此时服务器状态正确。但排队的 remove(T) 最终也会成功,并在链上晚于 add 到达,导致 ANS 删除了刚刚创建的注册。客户端缓存显示为 T,iOS 继续提供 T,因此“令牌是否已更改?”的检查永远不会触发。设备处于未注册状态且无法察觉。

通知会悄无声息地停止。唯一的恢复方法是在应用中手动切换 通知 → 关闭 → 推送,这会清除本地缓存并强制进行新的注册。

为什么不急于修复

这是一个真实的 Bug,但影响范围有限:它需要一次失败的注销,随后紧接着进行相同令牌的重新注册。掩盖此问题的两个捷径都比 Bug 本身更糟糕。HTTP 检查会将罕见的静默故障换成永久性的、普遍的元数据泄露。定期重新注册会按计划消耗用户资金,且对任何持有零 ADM 的用户完全无效。

正确的修复方法是客户端排序,且完全在区块链中介的设计范围内:使重试队列具备令牌感知能力,以便在后续的 add(T) 成功后丢弃挂起的 remove(T),停止将“缓存为空”视为注册状态,并像处理 remove 一样谨慎地持久化 add。这项工作应由客户端完成。

同一不对称性的相关后果是:unregister 只能针对应用程序仍记得的令牌发送,因此重新安装会导致之前的注册永久孤立。注册表目前在约 1,700 个不同地址中持有约 2,600 行数据,其中一个地址占用了 251 行。当 APNs 报告令牌失效时,行会被移除,因此对于仍在接收推送的用户,注册表确实会自我清理——但无人写入的地址永远不会被触发,也永远不会被清理。

基于此的构建建议

对于任何集成 ADAMANT 通知或构建客户端的人员,有两条建议。首先,不要添加设备到通知服务的回调——这是最自然的设计,但它会破坏隐私保证。其次,将注册和注销视为有序对。它们是异步的、可重试的、链上的,并且可能乱序到达。请显式地对它们进行排序,而不是从本地缓存中推断状态。