11.13. Сопряжение и привязка

Всё, что было рассмотрено до этого момента, передаёт байты по радиоканалу в открытом виде. Любой человек с ноутбуком, поддерживающим BLE, находящийся в той же комнате, может слушать каналы оповещения, отслеживать последовательность скачков открытого соединения и считывать каждую операцию чтения, записи и каждое уведомление, которые проходят по каналу. Для большинства общедоступных данных датчиков (уровень заряда батареи, температура окружающей среды) это нормально. Для всего, что обе конечные точки хотят сохранить в тайне – управляющий регистр, активирующий реле, пароль, измерение, которое не следует широко транслировать – канал должен быть зашифрован, и в идеале камера должна знать, с кем она разговаривает.

BLE обеспечивает и то, и другое через сопряжение и привязку.

11.13.1. Сопряжение, привязка, шифрование

Три тесно связанных понятия:

  • Шифрование – это конечная цель. Как только канал зашифрован, каждый пакет на каналах данных может быть расшифрован только двумя конечными точками; подслушивающий видит шум.

  • Сопряжение – это процедура, которую выполняют две конечные точки, чтобы договориться о ключах, используемых шифрованием. Это однократный обмен, который создаёт общий ключевой материал, подключаемый канальным уровнем к своему механизму шифрования.

  • Привязка – это решение сохранить ключи в энергонезависимом хранилище после завершения сопряжения, чтобы следующее соединение между теми же двумя устройствами пропускало сопряжение и сразу переходило к шифрованию.

Простыми словами: сопряжение – это «представьтесь друг другу»; привязка – это «запомните это знакомство»; шифрование – это «с этого момента говорите наедине».

Два столбца с подписями "Central" и "Peripheral". Пунктирная линия вверху с подписью "BLE connection open (unencrypted)". Под ней три стрелки: "pairing request" от центрального к периферийному, "key exchange" в обоих направлениях, "pairing complete" вперёд. Вторая пунктирная линия ниже с подписью "link encrypted". Две толстые двунаправленные стрелки несут "encrypted GATT traffic". Необязательный блок "store keys to flash" сбоку с подписью "bonding".

Поток сопряжения поверх открытого BLE-соединения. Как только обмен ключами завершается, канальный уровень шифрует каждый последующий пакет. Привязка – это дополнительный шаг записи ключей во флеш-память.

11.13.2. LE Secure Connections

Современный обмен ключами, используемый в BLE, – это LE Secure Connections, построенный на алгоритме Диффи-Хеллмана на эллиптических кривых. Обе стороны генерируют временную пару ключей, обмениваются открытыми половинами и объединяют результат со своими закрытыми ключами, чтобы получить один и тот же общий секрет – секрет, который подслушивающий не сможет вычислить даже при наличии полной записи обмена.

Более старый метод LE Legacy менее безопасен (подслушивающий, имеющий полную запись обмена, обычно может восстановить ключ) и существует только для обратной совместимости со старыми периферийными устройствами. По умолчанию в aioble используется современный метод (le_secure=True); оставьте его.

11.13.3. Инициирование сопряжения

Центральное устройство выполняет сопряжение, вызывая aioble.DeviceConnection.pair() на уже открытом соединении:

async with await device.connect() as connection:
    await connection.pair(bond=True, le_secure=True, mitm=False)
    # ... GATT work, now over an encrypted link ...

После возврата pair атрибуты encrypted, authenticated, bonded и key_size соединения отражают то, что было согласовано.

Четыре наиболее полезных именованных аргумента:

  • bond=True – сохранить полученные ключи во флеш-память, чтобы следующее соединение между теми же двумя устройствами пропускало рукопожатие сопряжения. По умолчанию True.

  • le_secure=True – использовать LE Secure Connections. По умолчанию True. Оставьте включённым.

  • mitm=False – требовать ли защиту от атаки посредника (man-in-the-middle). Это требует внеполосного канала (числовой код, отображаемый на одной стороне и подтверждаемый на другой, вводимый пароль доступа, …), чтобы пользователь мог убедиться, что два устройства в рукопожатии сопряжения действительно те, за кого он их принимает. По умолчанию False (без защиты от MITM – пассивный подслушивающий не может прочитать канал, но злоумышленник, активно перенаправляющий соединения, мог бы вклиниться в сопряжение). Установите True для всего конфиденциального, но учтите, что это требует, чтобы периферийное устройство действительно поддерживало возможность ввода-вывода.

  • io=3 – возможность ввода-вывода, заявляемая устройством. Спецификация Bluetooth определяет пять: 0 только дисплей, 1 дисплей + да/нет, 2 только клавиатура, 3 без ввода и вывода, 4 клавиатура + дисплей. Камера без пользовательского интерфейса обычно сообщает 3; если у самой камеры есть дисплей, приложение могло бы отображать числовое подтверждение и использовать 1. Сочетание возможностей ввода-вывода двух сторон определяет, достижима ли настоящая защита от MITM.

Периферийные устройства не вызывают pair сами – они отвечают на то, что инициирует центральное устройство. Требуется ли шифрование для данной характеристики – это свойство того, как она объявлена в базе данных GATT; биты доступа с требованием шифрования являются частью низкоуровневого API bluetooth и в настоящее время не предоставляются через конструктор характеристики aioble.

11.13.4. Привязка – и где хранятся ключи

Когда bond=True, aioble записывает ключи в JSON-файл в локальной файловой системе. Имя файла по умолчанию – ble_secrets.json, записываемый относительно текущего рабочего каталога. На только что загруженной камере _boot.py уже выбрал этот каталог: /sdcard, когда карта смонтирована, в противном случае /flash – так что файл оказывается по пути /sdcard/ble_secrets.json или /flash/ble_secrets.json. Файл содержит записи, необходимые для повторного шифрования канала при следующем переподключении привязанного устройства, включая идентификационный адрес устройства.

Одну асимметрию стоит держать в уме: сохранение происходит автоматически по мере изменения ключей, но загрузка файла при следующей загрузке – нет. Вызовите aioble.security.load_secrets() один раз при запуске (до любого сопряжения или оповещения), чтобы ранее привязанные устройства были распознаны:

import aioble
aioble.security.load_secrets()        # default path: ble_secrets.json

После этого, когда привязанное устройство появится в следующий раз, aioble повторно использует сохранённые ключи, и канал станет зашифрованным без дальнейшего рукопожатия.

Два практических следствия хранения ключей во флеш-памяти:

  • Забыть устройство. Удалите ble_secrets.json (или удалите соответствующую запись), чтобы забыть все привязанные устройства, а затем выполните сопряжение заново.

  • Физический доступ раскрывает ключи. Любой, кто имеет доступ к файловой системе камеры, может прочитать JSON. Это то же самое ограничение, что возникло на сетевой стороне с ключами TLS (Эксплуатация: ключи, истечение срока и устранение неполадок): используйте отдельные ключи для каждого устройства, считайте любой сохранённый ключ восстанавливаемым и полагайтесь на возможность отзыва (здесь – удаление привязки на стороне центрального устройства), а не на то, что ключ останется секретным.

11.13.5. Что гарантирует шифрование – и чего не гарантирует

Канал «сначала сопряжение, потом шифрование» даёт, в порядке убывания надёжности:

  • Конфиденциальность. Всегда. Подслушивающий не может прочитать байты.

  • Целостность. Всегда. Изменённые пакеты не проходят проверку аутентифицированного шифрования канального уровня и отбрасываются.

  • Аутентификация. Только при mitm=True и подходящем вводе-выводе. Без неё посредник, перехвативший исходный обмен сопряжения, мог бы вклиниться; без защиты от MITM у двух сторон нет способа об этом узнать.

Для большинства сценариев использования камеры – телефон один раз выполняет сопряжение с камерой, а затем подключается снова позже – mitm=False обычно достаточно, потому что исходное сопряжение происходит в контролируемой среде (пользователь держит оба устройства в одной комнате). Для приложений, где сопряжённое устройство может впервые встретить камеру на большом расстоянии или через недоверенного посредника, MITM – правильная настройка.

11.13.6. Когда сопряжение – неверный ответ

Сопряжение имеет реальную цену: несколько секунд обмена при первом подключении, постоянное использование флеш-памяти для каждого привязанного устройства и необходимость «забыть привязку», если что-то пойдёт не так. Для действительно общедоступных данных – показаний датчика окружающей среды, публикуемых в виде маяка, вывески, отображающей своё имя, всего, что не меняет мир от того, что его прочитали или записали – правильный ответ не шифровать вообще и позволить любому ближайшему сканеру читать значения.

Для всего остального connection.pair(bond=True) на центральном устройстве – это однострочное дополнение, превращающее канал из публичного в частный.