В 6.12, на которой снимался ESP-лог, в Android-коде этого вообще не было — никакого фонового time-sync с хозяйского телефона. Время отдавалось только как побочный эффект owner-fire (phase 3), и то не доезжало из-за disconnect ESP сразу после реле.
В 6.13 я добавил отдельный фоновой коннект: при каждом passive-scan ESP хозяйский телефон отдельной GATT-сессией пишет TIME. Но это всё ещё только хозяин.
Иначе: хозяин в отпуске → у ESP моргнул свет → ESP перезагрузился с rtc=0 → гости заблокированы навсегда. Это плохая архитектура.
Гость, у которого есть валидный токен, должен иметь право подписать TIME своим guest-ключом.
Идея простая: для гостя время может только идти вперёд. Хозяин же — доверенный, может сдвигать в обе стороны без ограничений (нужно когда кто-то по ошибке выставил 2099 и надо откатить).
ESP хранит в NVS последнее известное время (time_ratchet). На гостевой TIME-write:
new_time < ratchet → отвергаем как откат.new_time ≥ ratchet → принимаем, обновляем ratchet, пишем в NVS.На owner TIME-write ratchet просто перезаписывается значением хозяина (в любую сторону).
После reboot ESP стартует с rtc = ratchet (а не с 0). RTC считает дальше с этой точки. После factory-reset ratchet тоже сбрасывается — но pair-flow сразу пишет туда текущее время.
Атакующий гость хочет продлить свой токен (у которого valid_until = X). Что он может попытаться сделать?
Вариант 1 — отмотать назад (выставить время «вчера», чтобы свой токен казался свежим):
→ Ratchet режет. Отказ.
Вариант 2 — перемотать вперёд (выставить время «через год»):
→ ESP принимает (время реально пошло вперёд).
→ НО: теперь ESP сравнивает valid_until=X с now=через год → X < now → его собственный токен протух.
→ Атакующий выстрелил себе в ногу.
Единственный «честный» способ для гостя — выставить реальное текущее время. Любая подделка либо отвергается, либо вредит ему самому.
ESP должен принимать TIME write подписанный:
В TIME payload добавляем 2 байта bleId. ESP пробует:
Никакой attacker без легитимного guest-bundle (выданного через сервер) подписать не сможет — у него нет ключа.
Если с последнего успешного sync прошло менее 1 часа, ESP считает что его часы свежие и обновлять их незачем «просто так». Любой TIME-write в этом окне принимается только если клиент за последние 10 секунд успешно отстрелял FIRE (то есть доказал что у него валидный токен с непросроченным TTL).
Если прошло больше 1 часа — fire-gate отключён, принимаем TIME свободно (с потолком +24ч для гостей).
Что это закрывает: атакующий с украденным но истёкшим guest-bundle не может ничего сделать. Его FIRE отвергается по TTL → значит и TIME-write в свежем окне ему недоступен. А в «long-gone» окне его +24ч cap сам по себе ограничен — и хозяин при следующем проходе всё сбросит.
Если хозяин и три гостя одновременно пишут разное время — каждое последующее либо обновляет ratchet (если больше), либо отвергается (если меньше). Конкуренции нет, race-условий нет.
Прошивка ESP:
TimeCallback: попробовать verify owner_secret; если не сошлось — достать bleId из payload, деривировать guest_key, попробовать ещё раз.new_time ≥ time_ratchet.time_ratchet в NVS, обновлять при каждом accepted TIME.settimeofday(ratchet), g_rtcValid = (ratchet > 0).Android:
BleCrypto.buildTimeSync — добавить bleId, выбор ключа (owner_secret или guest_key).maybeSyncTime по аналогии с хозяйским (из passive-scan, дроссель 30 минут).После этого:
Если ESP стоит в зоне WiFi — самое надёжное решение это вообще убрать BLE-time-sync из критического пути и взять время напрямую с NTP-сервера.
Реализация в прошивке (NTP_ENABLED=1):
CHAR_WIFI — хозяин пишет туда подписанные SSID+password. Сохраняется в NVS.configTime() с pool.ntp.org / time.google.com / cloudflare.NTP_RESYNC_INTERVAL_MS (по умолчанию 6 часов).Для AA-батарейного устройства ESP большую часть времени спит и не может держать WiFi. Решение — на обычных wakeup'ах работает только BLE (короткое окно ~2.5с), а каждое DEEP_SLEEP_NTP_EVERY_N_WAKES просыпание расширяется до 20с и поднимает WiFi+SNTP. При wake=3с и N=7200 NTP-sync проходит примерно раз в 6 часов. Энергобаланс остаётся приемлемым.
Все параметры политики времени, sleep и NTP вынесены в CONFIG-блок наверху скетча. Под конкретный объект можно крутить:
FRESH_SYNC_WINDOW = 1ч, GUEST_FWD_CAP = 24ч, DEEP_SLEEP = on, NTP_EVERY_N = 7200.FRESH_SYNC_WINDOW = 5мин, GUEST_FWD_CAP = 15мин, DEEP_SLEEP = off (питание от 220В), NTP_ENABLED = 1.NTP_ENABLED = 0, остальное BLE-only.