在采集控制器日志的 6.12 版本里,Android 代码中根本没有这样的机制——不存在来自主人手机的后台授时。时间只是主人 fire(第 3 阶段)的副产品,而且还到不了:继电器一动,控制器立刻断开连接。
6.13 增加了独立的后台连接:每次被动扫描到控制器,主人的手机就开一条自己的 GATT 会话写入 TIME。但时间来源依然只有主人一个。
否则局面就是:主人在度假,控制器断电,重启后 rtc=0——访客只能等到主人回来。不能这么设计。
应允许持有有效令牌的访客用自己的访客密钥签署 TIME。
思路很简单: 对访客而言,时间只能向前走。主人是可信的,可以不受限制地双向调整——万一有人误设成 2099 年,就得能回退。
控制器把最后已知时间存在 NVS 里(time_ratchet)。访客写入 TIME 时:
new_time < ratchet → 视为回拨,拒绝。new_time ≥ ratchet → 接受,推进棘轮并写入 NVS。主人写入 TIME 时,棘轮直接被其值覆盖,方向不限。
重启后控制器从 rtc = ratchet 起步,而不是从 0。RTC 由此继续计时。恢复出厂设置也会清除棘轮,但配对会立刻把当前时间写进去。
发起攻击的访客想延长一个 valid_until = X)的令牌。他能试些什么?
方案 1——把时间往回拨 (把时钟设成昨天,让令牌看着还新鲜):
→ 棘轮不允许。拒绝。
方案 2——把时间往前拨 (把时钟拨快一年):
→ 控制器接受,因为时间确实向前走了。
→ 但控制器此刻比较的是 valid_until=X ,带 now = a year ahead → X < now → 于是他自己的令牌已经过期。
→ 攻击者只伤到了自己。
访客唯一诚实的做法就是设置真实的当前时间。任何伪造要么被拒,要么反噬自身。
控制器应接受由以下密钥签署的 TIME 写入:
TIME 载荷中增加两个字节: bleId。随后控制器依次尝试:
没有经服务器签发的合法访客捆绑包就无法签署 TIME——攻击者没有密钥。
如果上一次成功同步是在 不到一小时前,控制器就认为自己的时钟是新鲜的,没理由无缘无故更新。在该窗口内,只有客户端在过去 10 秒内完成过一次成功的 FIRE,TIME 写入才会被接受——这证明它持有未过期的有效令牌。
如果已经过了 一小时以上 fire 闸门关闭,TIME 可自由接受,访客仍受 +24 小时上限约束。
这能堵住什么:持有被盗但 已过期 访客捆绑包的攻击者无计可施。他的 FIRE 会因超期被拒,因此够不到新鲜窗口内的 TIME 写入。窗口之外的偏移依然被限制在 24 小时内,主人下次来时会把时间调正。
若主人和三位访客同时写入不同时间,每次写入要么因更大而推进棘轮,要么因更小而被拒。这里既无争用也无竞态。
控制器固件:
TimeCallback:先用 owner_secret 验证;失败则从载荷中取 bleId,派生 guest_key 再试一次。new_time ≥ time_ratchet.time_ratchet 存于 NVS,并在每次接受 TIME 时更新。settimeofday(ratchet), g_rtcValid = (ratchet > 0).Android:
BleCrypto.buildTimeSync ——增加 bleId 与密钥选择:owner_secret 或 guest_key。maybeSyncTime ,与主人路径类似,由被动扫描触发,且每 30 分钟至多一次。此后:
若控制器处于 Wi-Fi 覆盖范围内,更稳妥的做法是把 BLE 授时彻底移出关键路径,直接从 NTP 服务器取时间。
固件实现(NTP_ENABLED=1):
CHAR_WIFI ——主人在此写入经签名的 SSID 和密码,保存在 NVS 中。configTime() ,指向 pool.ntp.org、time.google.com 或 Cloudflare。NTP_RESYNC_INTERVAL_MS (默认 6 小时)再同步一次。用干电池供电时,控制器大部分时间在睡眠,无法维持 Wi-Fi。办法是:普通唤醒只跑 BLE,窗口约 2.5 秒;而每第 DEEP_SLEEP_NTP_EVERY_N_WAKES次唤醒延长到 20 秒,启动 Wi-Fi 并走 SNTP。当 wake=3 秒、N=7200 时,大约每 6 小时同步一次 NTP,能耗仍在可接受范围。
时间策略、睡眠与 NTP 的所有参数都在草图开头的 CONFIG 块里,可按现场调整:
FRESH_SYNC_WINDOW = 1 h, GUEST_FWD_CAP = 24 h, DEEP_SLEEP = on, NTP_EVERY_N = 7200.FRESH_SYNC_WINDOW = 5 min, GUEST_FWD_CAP = 15 min, DEEP_SLEEP = off (市电供电), NTP_ENABLED = 1.NTP_ENABLED = 0,其余一律只走 BLE。