Защита BLE-времени от подделки

Почему хозяин не передаёт время сейчас

В 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-ключом.

Как защититься от подделки — монотонный ratchet (только для гостей)

Идея простая: для гостя время может только идти вперёд. Хозяин же — доверенный, может сдвигать в обе стороны без ограничений (нужно когда кто-то по ошибке выставил 2099 и надо откатить).

ESP хранит в NVS последнее известное время (time_ratchet). На гостевой TIME-write:

На 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 → его собственный токен протух.
→ Атакующий выстрелил себе в ногу.

Единственный «честный» способ для гостя — выставить реальное текущее время. Любая подделка либо отвергается, либо вредит ему самому.

Кто подписывает TIME

ESP должен принимать TIME write подписанный:

В TIME payload добавляем 2 байта bleId. ESP пробует:

  1. HMAC с owner_secret — если ОК, принимаем.
  2. Иначе деривируем guest_key для этого bleId и проверяем.

Никакой attacker без легитимного guest-bundle (выданного через сервер) подписать не сможет — у него нет ключа.

Дополнительная защита: fire-first gate в «свежем» окне

Если с последнего успешного 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:

Android:

После этого:

NTP как первоисточник (опционально)

Если ESP стоит в зоне WiFi — самое надёжное решение это вообще убрать BLE-time-sync из критического пути и взять время напрямую с NTP-сервера.

Реализация в прошивке (NTP_ENABLED=1):

NTP + deep-sleep вместе

Для 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-блок наверху скетча. Под конкретный объект можно крутить: