BLE-реле — модель угроз

Что может злоумышленник, знающий исходники прошивки, но не имеющий доступа к секретам устройства.

Поверхность атаки

Прошивка публична, поэтому форматы пакетов, длины и алгоритмы (X25519, HMAC-SHA256, HKDF) известны. BLE — открытый эфир, поэтому объявления и записи GATT можно слушать. Ни в прошивке, ни в эфире нет ни одного секрета.

Где живут ключи:

Что злоумышленник не может

AttackDefence
Подделать FIREHMAC-SHA256. Без owner_secret или guest_key это перебор 2128 — перебор неосуществим.
Подделать запись TIMEТот же HMAC по bleId + epochMs.
Повторить подслушанное объявлениеСчётчик монотонный: при повторе или уменьшении HMAC не сходится.
Повторить подслушанный fireКольцо nonce гасит уже использованные значения.
Отвести часы контроллера назадХраповик: любое TIME < храповика отвергается. Храповик лежит в NVS и переживает отключение питания.
Использовать просроченный бандлПосле работы контроллера храповик не меньше последнего реального времени. У просроченного бандла validUntil ниже храповика, поэтому fire отсекается по сроку.
Перевести часы на год вперёд и отрезать всех гостейОграничение +24 ч на одну гостевую запись TIME. У владельца ограничения нет — владельцу доверяем.
Спариться вторым владельцем поверх первогоПосле сопряжения контроллер держит owner_secret в NVS и не входит в режим сопряжения без сброса к заводским настройкам.

Что злоумышленник может и чем это грозит

1. Перевод времени вперёд — отказ в обслуживании для законных гостей

Requires: действующий непросроченный гостевой бандл.

Action: каждый раз в зоне действия писать TIME со скачком на 24 часа вперёд.

Limit: +24 ч за сеанс, причём нужен свежий fire, если последняя синхронизация была меньше часа назад. После десяти визитов храповик уйдёт на десять дней вперёд, и токены остальных законных гостей будут выглядеть просроченными.

Обратная сторона: токен самого злоумышленника сверяется с тем же фальшивым временем и из-за этого протухает раньше срока.

Что делать: владелец подходит и пишет настоящее время. Храповик не сдвинется — назад он не ходит, — но новые гостевые токены выдаются с актуальным validUntil и дальше работают у всех. Для восстановления достаточно обновить срок действия на сервере.

Severity: временный отказ в обслуживании до прихода владельца. Не критично.

2. Человек посередине во время сопряжения

Requires: физически находиться в зоне действия контроллера в 90-секундном окне после того, как владелец включил сопряжение.

Action: подменить в рукопожатии сопряжения собственный открытый ключ X25519.

Result: контроллер спарится со злоумышленником, а не с владельцем. Владелец будет считать сопряжение удавшимся, но с телефона ничего не откроет.

Текущая защита: только временное окно и физическое присутствие владельца — он видит, что сопряжение не подтвердилось, и повторяет его.

Hardening: добавить простой код подтверждения — четыре цифры на экране контроллера или в Serial, — который владелец подтверждает в приложении. Сегодня такого кода нет.

Severity: окно узкое и требует одновременно близости и попадания в те самые 90 секунд. На практике трудноисполнимо.

3. Физический сброс к заводским настройкам

Requires: физический доступ к контроллеру и долгое нажатие кнопки.

Action: NVS стирается, owner_secret удаляется, контроллер возвращается в режим сопряжения.

Result: злоумышленник спарит контроллер с собой, а владелец потеряет доступ.

Defence: ставить контроллер в защищённом месте — в коробе, под крышкой.

Severity: то же самое, что сорвать домофон с двери, — это не криптографическая задача.

4. Извлечение owner_secret через UART или JTAG

Requires: физический доступ и дамп флеша при выключенных secure boot и шифровании флеша — сейчас они выключены.

Action: прочитать NVS, где лежит owner_secret.

Result: полная компрометация устройства и всех гостевых токенов.

Defence: включить secure boot и шифрование флеша ESP32 (efuse, необратимо). Сегодня не включено.

Severity: приемлемо для бытового применения. На ответственных объектах secure boot нужен.

5. Захват GATT-соединений — отказ в обслуживании

Requires: находиться в зоне действия BLE.

Action: подключиться и не отключаться. NimBLE держит лишь ограниченное число одновременных соединений.

Result: законные клиенты не могут подключиться, пока злоумышленник держит слот.

Текущая защита: только тайм-ауты NimBLE.

Hardening: добавить тайм-аут простоя: если клиент за 5 секунд не выполнил корректный FIRE, рвать соединение.

Severity: временный отказ в обслуживании, пока злоумышленник рядом.

6. Поток некорректных записей (подмена соединения)

Action: забрасывать контроллер мусорными записями.

Result: контроллер их отбрасывает и пишет в Serial. В его состоянии ничего не меняется, а стоит злоумышленнику уйти — всё работает как обычно.

Что защитой сознательно не является

Три места выглядят защитой, но ею не являются. Лучше назвать их прямо, чем дать кому-то на них опереться.

Байт состояния в объявлении несёт 16-битный тег. Этого хватает против случайного искажения в эфире и не более: упорный подделыватель переберёт 16 бит за секунды. Байт управляет только индикатором открыто/закрыто на карточке и никаких прав не даёт, поэтому тег и остаётся коротким — каждый лишний байт стоит батареи на каждом пробуждении.

Ключи только для приложения опираются на секрет, вшитый в APK. Кто распакует файл, тот получит секрет и подделает аттестацию. Это поднимает планку перед случайным веб-клиентом, не более. Настоящий контроль для такого ключа — привязка к отпечатку одного устройства: эта проверка живёт на сервере и не зависит от того, хранит ли приложение секрет.

Срок действия гостевого ключа для BLE обеспечивает сам контроллер: в токене есть срок, а у контроллера есть часы. У остальных типов объектов срока нет вовсе — ключ живёт, пока владелец его не отзовёт. Приложение обратного и не утверждает, но если ограничение по времени когда-нибудь появится в интерфейсе и для них, обеспечивать его придётся на сервере, а не рисовать на карточке.

Итого

Стандартное усиление для промышленного применения:

  1. Secure boot и шифрование флеша (защита от JTAG и дампов).
  2. Код подтверждения в сценарии сопряжения (защита от человека посередине).
  3. Тайм-аут простоя на GATT-соединениях (защита от захвата).
  4. Сравнение подписей за постоянное время — сделано и в прошивке, и в приложении: прежнее побайтное сравнение выдавало временем ответа, сколько байт подписи угадано.

Ничего из этого не критично для сегодняшних сценариев, но ответственные объекты усиления заслуживают.