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

Почему сегодня время не приносит владелец

В версии 6.12, с которой снят лог контроллера, в коде Android ничего подобного не было — фоновой синхронизации времени с телефона владельца не существовало. Время уходило лишь побочным эффектом владельческого fire (фаза 3), да и то не доезжало: контроллер рвал соединение сразу после срабатывания реле.

В 6.13 появилось отдельное фоновое подключение: при каждом пассивном обнаружении контроллера телефон владельца открывает свою GATT-сессию и пишет TIME. Но источник времени по-прежнему один — владелец.

Приносить время должен уметь любой доверенный клиент

Иначе картина такая: владелец в отпуске, у контроллера пропало питание, тот перезагрузился с rtc=0 — и гости отрезаны до его возвращения. Так строить нельзя.

Гостю с действующим токеном нужно позволить подписывать TIME своим гостевым ключом.

Как не дать подделать — монотонный храповик, только для гостей

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

Контроллер держит последнее известное время в NVS (time_ratchet). При гостевой записи TIME:

При владельческой записи TIME храповик просто перезаписывается значением владельца, в любую сторону.

После перезагрузки контроллер стартует от rtc = ratchet , а не от 0. RTC считает дальше от этой точки. Сброс к заводским настройкам стирает и храповик, но сопряжение сразу же записывает в него текущее время.

Почему это останавливает злонамеренного гостя

Атакующий гость хочет продлить токен, у которого valid_until = X). Что он может попробовать?

Вариант 1 — отвести время назад (выставить вчерашний день, чтобы токен выглядел свежим):
→ храповик не пускает. Отказ.

Вариант 2 — перевести время вперёд (выставить год вперёд):
→ контроллер принимает: время действительно пошло вперёд.
→ НО контроллер теперь сравнивает valid_until=X с now = a year aheadX < now → и его собственный токен просрочен.
→ Злоумышленник навредил только себе.

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

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

Контроллер должен принимать запись TIME, подписанную:

В полезную нагрузку TIME добавляются два байта: bleId. Дальше контроллер пробует:

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

Без законного гостевого бандла, выданного через сервер, подписать TIME нельзя — ключа у злоумышленника нет.

Дополнительная защита: гейт сначала fire внутри свежего окна

Если последняя удачная синхронизация была меньше часа назад, контроллер считает свои часы свежими и не видит причин обновлять их ни с того ни с сего. Внутри этого окна запись TIME принимается, только если клиент за последние 10 секунд выполнил удачный FIRE, — это доказывает, что у него есть действующий токен с непросроченным сроком.

Если прошло больше часа гейт fire выключается и TIME принимается свободно, всё так же с потолком +24 ч для гостей.

Что это закрывает: злоумышленник с украденным, но просрочено гостевым бандлом ничего не сделает. Его FIRE отсекается по сроку, значит запись TIME внутри свежего окна недоступна. Вне окна сдвиг всё равно ограничен сутками, а владелец при следующем визите выставит верное время.

Бонус: никаких коллизий

Если владелец и три гостя одновременно пишут разное время, каждая запись либо двигает храповик, если она больше, либо отвергается, если меньше. Ни борьбы, ни гонки здесь нет.

Что нужно сделать

Прошивка контроллера:

Android:

После этого:

NTP как основной источник (необязательно)

Если контроллер стоит в зоне Wi-Fi, надёжнее вовсе убрать синхронизацию времени по BLE с критического пути и брать время прямо с NTP-сервера.

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

NTP вместе с глубоким сном

На батарейках контроллер спит почти всё время и держать Wi-Fi не может. Решение: обычные пробуждения работают только по BLE, коротким окном около 2,5 с, а каждое DEEP_SLEEP_NTP_EVERY_N_WAKES-е пробуждение растягивается до 20 с и поднимает Wi-Fi с SNTP. При wake=3 с и N=7200 синхронизация по NTP случается примерно раз в 6 часов, а расход энергии остаётся приемлемым.

Настраиваемые константы прошивки

Все параметры политики времени, сна и NTP лежат в блоке CONFIG в начале скетча. Их можно подстроить под объект: