В версии 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 нельзя — ключа у злоумышленника нет.
Если последняя удачная синхронизация была меньше часа назад, контроллер считает свои часы свежими и не видит причин обновлять их ни с того ни с сего. Внутри этого окна запись TIME принимается, только если клиент за последние 10 секунд выполнил удачный FIRE, — это доказывает, что у него есть действующий токен с непросроченным сроком.
Если прошло больше часа гейт fire выключается и TIME принимается свободно, всё так же с потолком +24 ч для гостей.
Что это закрывает: злоумышленник с украденным, но просрочено гостевым бандлом ничего не сделает. Его FIRE отсекается по сроку, значит запись TIME внутри свежего окна недоступна. Вне окна сдвиг всё равно ограничен сутками, а владелец при следующем визите выставит верное время.
Если владелец и три гостя одновременно пишут разное время, каждая запись либо двигает храповик, если она больше, либо отвергается, если меньше. Ни борьбы, ни гонки здесь нет.
Прошивка контроллера:
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 синхронизация по NTP случается примерно раз в 6 часов, а расход энергии остаётся приемлемым.
Все параметры политики времени, сна и 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.