English — a minimal, auditable implementation of Apple-style Continuity between
Windows 10/11 and Android: mirrored notifications (both directions, with replies), a
shared clipboard, file transfer and handoff, over a single TLS-encrypted local protocol.
Pairing is a six-digit SAS plus certificate pinning; there is no cloud, no account and no
third-party dependency. This document is in Chinese; docs/ mixes both.
把 iPhone ↔ Mac 的"连续互通"语义,用公开、可审计的协议搬到 Windows 10/11 ↔ Android 上。
不是模拟 Apple 的私有链路(AWDL / BLE 广告 / iCloud 身份),而是先把这些链路各自解决什么问题拆出来,再用两端都有的标准件逐一替换,并明确标出哪些东西换不掉。
Apple 把这一组能力叫 Continuity(连续互通)。它不是一条协议,而是四层拼起来的:
| 层 | 用的是什么 | 解决的问题 |
|---|---|---|
| 身份与信任 | 同一 Apple ID;设备间通过 APNs 带外完成 BLE 4.2 配对,之后每台设备生成一把 256-bit AES 密钥存进钥匙串(Apple 平台安全指南) | 两台设备如何确信"对方是我的" |
| 发现与测距 | BLE 广告里的 Continuity TLV(Handoff 0x0C、Nearby Info 0x10 等),配合旋转地址防跟踪;iPhone 11+ 另用 UWB(U1/U2)做指向性选择 |
谁在附近、该把图标显示给谁 |
| 数据面 | AWDL(Apple Wireless Direct Link):自研 802.11 ad-hoc,action frame 同步、6/44/149 社交信道、约 1 秒周期的可用窗口;AirDrop / Handoff 大载荷 / Sidecar / 隔空播放都跑在上面。载荷是 IPv6,DNS-SD 被塞进 Wi-Fi action frame | 不经过路由器也能高速直连 |
| 应用语义 | NSUserActivity(activityType + userInfo ≤3 KB + webpageURL)、通用剪贴板、通知/通话/SMS 中继、iPhone 镜像 |
传什么、由哪个 App 接 |
几个常被误解的点:
- Handoff 不是"传文件"。它传的是一个
activityType加最多几 KB 的状态,接收端按 Team ID + activityType 找到同一个开发者的 App 来续接;大载荷(比如带附件的邮件草稿)才升级到 AWDL + TLS。 - 通用剪贴板跑在 Handoff 之上:剪贴板写入被当作一次 activity,内容经 Wi-Fi + TLS 传输,实测有效期约 2 分钟(Apple 只说"短时间内")。
- iPhone 镜像(macOS 15)不是 AWDL。Apple 只说是"本地点对点协议 + iCloud 端到端密钥",公钥在 Secure Enclave 里;外围观察指向 LLW(低延迟 Wi-Fi)接口,但 Apple 从未文档化。
- 通知:macOS 15 之前只中继电话和 SMS;"所有 iPhone 通知上 Mac"是 iPhone 镜像带来的,按 App 逐个开关。
细节与可信度分级见
docs/apple-continuity.md;该文把"官方文档 / 逆向研究 / 推测"分开标注。
| Apple 的做法 | 在 Windows/Android 上有对应物吗 |
|---|---|
| iCloud 身份 + APNs 带外配对 | 没有。没有跨厂商的身份根,只能退化成"人对六位码"(SAS),这正是本项目采用的办法 |
| BLE 广告做发现 | 部分有:Android 有 BLE 广播 API,Windows 桌面端没有可靠的广播接口。本项目改用 UDP 广播信标,语义等价("我在,端口是这个"),拿不到 RSSI 测距 |
| AWDL 直连 | 没有。两端都不开放 802.11 ad-hoc。只能用普通 Wi-Fi LAN + TCP/TLS |
| Team ID 决定谁来续接 | 没有跨平台 App 身份体系。本项目改成 Windows 侧一张 activityType → 命令行模板 的路由表 |
| Secure Enclave 保护长期密钥 | Windows 有 TPM、Android 有 Keystore,但跨厂商无法互信;本项目用应用层密钥 + 证书指纹 pinning |
结论:能 1:1 复刻的是"语义",不是"链路"。凡是依赖 Apple 身份根或私有无线的部分,都必须换成"用户参与的一次性验证"。
┌─────────────────── Windows 10/11 ───────────────────┐ ┌────────────── Android 10+ ──────────────┐
│ UserNotificationListener ─┐ │ │ NotificationListenerService ─┐ │
│ wpndatabase.db (历史/兜底) ├─► WindowsNotification │ │ ├─► NotifListener │
│ Win32 clipboard + 序号 │ Reader │ │ ClipboardManager │ │
│ HandoffRouter(activityType) ┘ │ │ Share sheet / intents ┘ │
│ │ │ │ │ │
│ Bridge ◄── 统一扇出/去重 ──► │ │ BridgeBus │
│ │ │ │ │ │
│ AgentServer TLS1.2/1.3 TCP 47820 ◄───────────────┼────────┼──► BridgeClient TLS + 证书 pinning │
│ UDP beacon 47821 (发现) │ LAN │ UDP listener 47821 (发现) │
│ control 127.0.0.1:47822 (仅调试/测试) │ │ ForegroundService (dataSync) │
└──────────────────────────────────────────────────────┘ └──────────────────────────────────────────────────┘
| iPhone ↔ Mac | 本项目 | 状态 |
|---|---|---|
| Handoff(按 activityType 续接) | handoff 帧 + Windows 侧 activityType → 命令模板;手机通过分享菜单把链接/文本交给 PC |
✅ 双向已验证 |
| 通用剪贴板 | clipboard 帧,双向;Win32 序号轮询,回写抑制回声 |
✅ 双向已验证(含回环一致性) |
| 通知镜像 | Windows toast → Android 通知(UserNotificationListener + toast XML);Android NotificationListener → PC |
✅ 双向已验证(含真实系统 toast) |
| 通知回复 | reply:true 能力位 + notif-reply 帧;Android 侧填充 RemoteInput 并触发原通知的 action |
✅ 已验证(PC 输入 → 应用回调) |
| BLE 广告 / 附近设备 | UDP 广播信标(3 秒一次,只含端口/主机名/指纹,不含身份) | |
| 带外配对 | ECDH P-256 + HKDF → 六位 SAS 人工比对 + 证书指纹 pinning | ✅ 已验证(错误密钥被拒) |
| AirDrop 文件传输 | file-* 帧:offer → accept → 256 KiB 分块 → SHA-256;手机落到系统 Downloads/ContinuityBridge |
✅ 双向已验证(逐字节哈希比对) |
| Sidecar / 屏幕镜像 | 未实现 | ❌ |
| 通话 / SMS 中继 | 未实现(Android 侧需要额外权限与运营商通道) | ❌ |
为什么剪贴板要"人工比对六位码":TLS 用的是自签名证书,同一局域网里的主动攻击者可以插在中间换掉自己的 ECDH 公钥。两端各显示同一个六位数,人眼一比,MITM 就变成百万分之一的概率——这就是 Apple 用 APNs 带外通道做的事,本地网络里只能由用户来当这条带外通道。
需要 .NET 8 SDK。
cd windows/ContinuityBridge.Agent
dotnet build -c Release
# 交互模式(打印证书指纹与日志)
dotnet run -c Release
# 无人值守(服务/测试用),并开放本机控制口
dotnet run -c Release -- --headless --control-port 47822参数:
| 参数 | 含义 |
|---|---|
--port N |
TLS 监听端口,默认 47820 |
--discovery-port N |
UDP 信标端口,默认 47821 |
--data-dir PATH |
证书、已配对设备、设置的存放目录(默认 %LOCALAPPDATA%\ContinuityBridge) |
--poll-seconds N |
剪贴板/通知轮询间隔,默认 1 |
--headless |
无交互输出;启用本机控制口 |
--control-port N |
控制口端口,默认 47822,只绑 127.0.0.1 |
--history N |
直接从 wpndatabase.db 读 N 条历史通知后退出 |
控制口(仅调试;给端到端测试和演示用):
status
notify <app>|<title>|<body>
clip <text>
sendfile <path>
reply <notification-id> <text>
需要 JDK 17–21(JDK 25 会让 Gradle 8.13 直接崩在 class file 69,这是实测坑)、Android SDK 35、Gradle 8.13。
cd android
JAVA_HOME=/path/to/jdk-21 ./gradlew assembleDebug
adb install -r app/build/outputs/apk/debug/app-debug.apk
# 通知访问(否则手机通知发不出去)
adb shell cmd notification allow_listener dev.continuitybridge/.NotifListener
adb shell pm grant dev.continuitybridge android.permission.POST_NOTIFICATIONS打开 App:填入 PC 地址(模拟器用 10.0.2.2;真机填局域网 IP),点 Pair。
PC 控制台会打印 paired '...', SAS 123456,手机上弹出同样的六位数——一致才点 "It matches"。
python -m pip install cryptography
python tools/e2e_test.py --agent windows/ContinuityBridge.Agent/bin/Debug/net8.0-windows10.0.19041.0/ContinuityBridge.Agent.exe这个测试不复用任何 C# 代码,按 docs/protocol.md 独立实现客户端,覆盖:配对/ SAS 一致性 / 重连 / 错误密钥拒绝 / 剪贴板 / Handoff 路由 / 心跳 / 无版本帧拒绝。
Java 协议层也可以脱离 Android 直接跑:
javac -d /tmp/jc $(find android/app/src/main/java tools/java -name '*.java')
java -cp /tmp/jc Probe 127.0.0.1 47820 /tmp/probe-state- 传输:TLS 1.2/1.3,服务端 ECDSA P-256 自签名证书,首次配对后把证书 SHA-256 pin 到手机上;指纹变化直接拒绝连接。
- 配对:双向 ECDH(服务端临时密钥 × 客户端临时密钥),
shared = SHA-256(X),HKDF-SHA256派生pairKey与 SAS。双方先验 MAC(确认对方真在同一 ECDH 秘密上),再让用户比对 SAS。 - 重连:HMAC-SHA256 挑战应答,双方各出 16 字节随机 nonce,双向出证;重放旧握手没有用。
- 剪贴板/通知:只走已认证会话;不落盘(配对状态除外),不经过任何云。
- 控制口:只绑回环,仅 headless 模式启用;它能注入通知,属于本地调试面,不是网络面。
- 未做:前向保密仅由 TLS 提供;协议未经第三方审计;六位 SAS 的强度依赖用户真的去比对。
详见 docs/protocol.md。
已验证(2026-10-23,Windows 10 19045 + Android API 35 模拟器)
| 项 | 证据 |
|---|---|
| 协议 17 项断言全过 | python tools/e2e_test.py → 17 passed, 0 failed |
| Java 协议层与 C# 互通 | Probe 五步全过(配对/认证/剪贴板/Handoff/重连) |
| 真实 Windows toast → 手机通知 | 手机日志 mirrored: [Windows] Windows toast;截图 docs/screenshot-app.png |
| PC → 手机剪贴板 | 手机日志 clipboard received (13 chars) |
| 手机 → PC 剪贴板 | PC 日志 clipboard from ...: pc-...,Windows 剪贴板读回一致 |
| 手机通知 → PC | PC 日志 PHONE_NOTIFICATION Shell Shell ... |
| 手机 Handoff → PC | PC 日志 handoff 'dev.continuitybridge.browse' -> cmd.exe,手机收到 handoff accepted |
| PC → 手机文件 | 400 KiB 文件;手机日志 received … -> Downloads/ContinuityBridge,adb pull 后 SHA-256 与源文件一致 |
| 手机 → PC 文件 | 256 KiB 文件;PC 日志 received e2e-262144.bin (262144 bytes)(接收端校验通过才会写这一行) |
| 通知回复 | PC reply <key> reply-from-pc-ok → 手机 reply sent → 应用回调 REPLY_RECEIVED:reply-from-pc-ok |
| 错误密钥被拒绝 | e2e 第 3 项;服务端返回 auth-failed 后主动断连 |
| 交付物静态核验(REA) | 见 docs/verification.md |
未验证 / 明确不做
- UDP 发现:模拟器 NAT 收不到宿主机广播;真机需要同一 LAN 且允许广播。
- Android 10+ 限制:后台读剪贴板被系统禁止,所以"手机→PC 剪贴板"只在 App 位于前台时可靠(本项目用按钮触发);写剪贴板在后台也可能被静默丢弃,失败时值会留在 App 内供手动复制。
- 屏幕镜像、通话/SMS 中继、BLE/UWB 测距、剪贴板图片均未实现。
- 文件传输未做断点续传(协议里明确写了原因);也没有和 LocalSend 客户端互通——
cb1自带信任,LocalSend 有另一套会话模型,混用会多开一个未审计面。 - 只在 Windows 10 19045 与 API 35 模拟器上跑过;未在真机、Windows 11、Android 14 以下验证。
continuity-bridge/
├─ windows/ContinuityBridge.Agent/ Windows 端(.NET 8,无第三方 NuGet)
│ ├─ Core/ 证书、ECDH/HKDF/SAS、帧、TLS 会话、配对与重连
│ ├─ Platform/ Win32 剪贴板、通知读取(API + wpndatabase.db)、Handoff 路由
│ └─ Services/ Bridge 扇出、控制口
├─ android/ Android 端(纯 Java,零 AndroidX/Compose 依赖)
│ └─ app/src/main/java/dev/continuitybridge/
│ ├─ Crypto.java / Json.java / Wire.java 协议层(可在 JVM 上单独跑)
│ ├─ BridgeClient.java 配对/认证/连接状态机
│ ├─ BridgeService.java / NotifListener.java 前台服务、通知中继
│ └─ MainActivity.java / BridgeBus.java 单页 UI 与状态总线
├─ tools/
│ ├─ e2e_test.py 独立实现的协议测试(Python)
│ └─ java/Probe.java JVM 协议探针
└─ docs/
├─ apple-continuity.md Apple 侧机制与可信度分级
├─ protocol.md 线协议规范
├─ verification.md 测试与 REA 证据
└─ rea-evidence.json REA 导出的证据包(6 条记录)
文件投放已完成(无断点续传);可选增强:与 LocalSend v2.2 互通,复用其prepare-upload/upload端点。- 发现增强:mDNS/DNS-SD(
_continuitybridge._tcp)+ 真机 BLE 信标;Windows 侧改用 Wi-Fi Direct 或保持 LAN。 - 可信设备管理 UI:列出已配对设备、按能力授权(通知/剪贴板/Handoff 分开)、一键吊销。
- 通知回复的桌面 UI:现在只能通过控制口回复;下一步在 Windows 侧加一个跟随通知的输入框。
- 剪贴板补全:图片(DIB ↔ PNG)、Android 输入法插件以绕开后台限制。
- 屏幕镜像:封装 scrcpy(或 App-Scrcpy 的无 ADB 路线),不自研编码。
MIT — see LICENSE.