Кадр из фильма «Назад в будущее» (1985 год)
Стандарт Bluetooth 5.0 вышел в 2016 году, 2019-м появилась версия 5.2. За последнее время Apple провела две конференции WWDC 2017, WWDC 2019 посвященных CoreBluetooth. Активно развивается технология построения mesh сетей. Все стало еще лучше, быстрее и эффективнее. Интерес к этому направлению только растет. Выстроены целые системы управления на этой технологии.
Мы же задались целью автоматизировать рутинные операции и повысить безопасность доступа пользователей на свое рабочее место. В статье разберем, что было решено предложить пользователям, поговорим немного о технологии BLE (хотя, как тут кратко?) на примере небольшого проекта, который запускается на двух смартфонах и позволяет передавать данные в обе стороны, ну а в конце познакомлю с нашим приложением GM MOBILE ASSISTANT.
Я работаю swift-разработчиком в компании Гетмобит. Мы создаем ИТ-инфраструктуру а-персональных рабочих мест, которая позволит изменить традиционное восприятие рабочей среды, станет более гибкой и независимой от локации, где много внимания уделяется вопросам обеспечения безопасности. Наша экосистема называется GM SMART SYSTEM, ее ключевой элемент — устройство GM-Box, сочетающее тонкий клиент и VOIP телефонию. Более подробно, с нашей инфраструктурой, можно ознакомиться в статьях моего коллеги.
Что нам захотелось улучшить?
Не секрет, что во многих компаниях политика корпоративной безопасности запрещает использование коротких, читабельных паролей. Обязательна определенная длина, спецсимволы, использование заглавного регистра. Все эти звездочки, решетки, подчеркивания приходится вводить при каждом входе. Когда такие пароли начинают записывать и хранить на бумажке рядом с рабочим местом, такая «безопасность» явно рискует выйти боком… В некоторых компаниях практикуется использование USB-токенов, а к экзотике можно отнести NFC/RFID смарт-карты. Такие сложные решения требует затрат, внедрения и техподдержки. При этом, токен или карту вполне могут украсть, получив доступ к чувствительным данным.
Мы решили избавиться от этих проблем, сделав доступной авторизацию через мобильный телефон. Решение основано на том, что пользователь однажды ввел свои учетные данные в приложении на смартфоне и может забыть про ручной ввод. Мобильный телефон есть у каждого пользователя, приложение доступно в AppStore и Google Play и является частью GM SMART SYSTEM, т.е. работает из «коробки», не требует затрат на поддержку. Данные хранятся в защищенной области, доступ к приложению закрыт пин-кодом или биометрией, передаваемые данные шифруются. Мы понимаем, что любую систему можно взломать, но всегда встает вопрос целесообразности.
Работа над решением
Определив смартфон, как средство для авторизации в нашей инфраструктуре, встал вопрос выбора кроссплатформенного решения iOS/Android. Не во всех компаниях разрешено использование WI-FI, USB API не доступно для iOS без дополнительной головной боли, Bluetooth порезан до BLE опять же в iOS. Была даже идея использовать звук… но это отдельная история. По итогу, из всего того, что можно, наиболее практичным показался BLE. Пошли изучать матчасть.
Исследования
После изучения довольно большого количества информации, пришли к выводу, что важно хорошо понимать базовые принципы взаимодействия BLE устройств. Поэтому постарался максимально точно отобразить их на схеме ниже, она применима для любой платформы.
Технология BLE реализует клиент-серверную модель, является самостоятельным сегментом Bluetooth, рассчитана на низкое энергопотребление и относительно небольшую пропускную способность передачи данных. Вполне достаточно для обмена небольшими пакетами.
На схеме пунктиром выделены несколько Peripheral (GATT server), которые ожидают входящие подключения и что бы заявить о себе в радиоэфире, после инициализации, Peripheral, запускает процесс вещания Advertising пакетов с заданной частотой, чем чаще, тем выше вероятность обнаружения. Central (GATT client) запускает процесс поиска (как правило, запускается на несколько секунд) и пытается найти все устройства в радиусе 10-1500 (Bluetooth 5) метров. Дистанция, конечно, сильно зависит от преград и помех. После обнаружения получаем возможность подключиться, прочитать профиль устройства, подписаться на характеристики (тип NOTIFY), передать данные (WRITE). Любой атрибут имеет уникальный uuid идентификатор.
GATT (Generic Attribute Profile) — профиль является общей спецификацией для отправки и получения коротких фрагментов данных, строится на основе протокола атрибутов АТТ. Intro to Bluetooth Generic Attribute Profile.
ATT (Attribute Protocol) — протокол атрибутов, оптимизированный для работы на BLE-устройствах. Использует настолько мало байт, насколько это возможно. Каждый атрибут идентифицируется уникальным универсальным идентификатором UUID.
Service — совокупность характеристик.
Characteristic — по сути, это канал для передачи данных, ограниченного заданной функциональностью:
- READ — после подключения можно прочесть предварительно заданное значение.
- WRITE — клиент использует характеристику такого типа для передачи данных на сервер, однонаправленный канал.
- NOTIFY — используется для асинхронного приема данных от сервера, клиент должен подписаться на получение.
- INDICATE — используется для получения данных, не требует подписки, но необходимо запросить значение.
Descriptor — используется для описания характеристики, необязательный атрибут.
UUID — стандартизированный 128-битный строковый идентификатор, используемый для однозначной идентификации информации. Может быть задан в сокращенном виде, например 180A — информация об устройстве. Обнаружив сервис с таким идентификатором, мы должны предположить, что имеющиеся характеристики будут выдавать нам информацию об этом устройстве. Подробнее в этой статье. Можно воспользоваться генератором uuid.
Adveritising — короткие пакеты, необходимы для обнаружения BLE Peripherals. Частота появления в радиоэфире зависит от выставленного значения, минимально от нескольких мс. Дополнения в bluetooth 5.
Перечень зарезервированных профилей. Перечень зарезервированных 16 бит идентификаторов.
Пробуем на практике
Чтобы лучше понять приведенную выше схему, написал небольшое приложение. В проекте используется Core Bluetooth фреймворк. Запускать нужно на 2-х телефонах. На первом выбираем GATT сервер, на втором GATT клиент. После подключения можно пересылать текстовые сообщения. Ниже, разберем по порядку ключевые моменты создания профилей.
Настройка проекта
После создания нового проекта в Xcode добавляем ключ в info.plist
<key>NSBluetoothAlwaysUsageDescription</key>
<string> Описание для чего будет использоваться bluetooth в приложении</string>
Это нужно, чтобы пояснить пользователю для чего используется bluetooth и получить разрешение. После первого запуска появится диалог с сообщением указанным в описании ключа.
Важным моментом, особенно для профиля peripheral, является работа в фоновом режиме, если его не включить, то приложение не будет отправлять Advertising пакеты при сворачивании или выключенном экране. Для активации нужно добавить Background Modes и выбрать два пункта, так как мы хотим работать с обоими профилями.
Создаем профиль GATT сервер
Для управления профилем нам потребуются два класса: CBPeripheralManager, CBPeripheralManagerDelegate. Атрибуты задаются с помощью: CBMutableService, CBMutableCharacteristic, CBMutableDescriptor. Причем созданные атрибуты вкладываются один в другой по цепочке дескриптор, характеристика, сервис.
В начале создадим UUID для каждого атрибута планируемого профиля.
static let primaryServiceUUID =
CBUUID(string: "bf52e2d6-ff52-43cb-99a0-872fa3dde94f")
static let readCharacteristicUUID =
CBUUID(string: "f438775d-e605-42b8-abe7-6e31ed52ea87")
static let transferCharacteristicUUID =
CBUUID(string: "a82ae020-e171-42f8-a31c-5f612926f041")
Затем нужно создать две характеристики. Первая рассчитана только на передачу некоторой предварительно подготовленной информации. Central сможет считать данные после подключения.
var readCharacteristic =
CBMutableCharacteristic(type: UUIDs.readCharacteristicUUID,
properties:[.read],
value:"some identificator".data(using: .utf8),
permissions:[.readable])
Вторая будет выполнять роль канала передачи данных. Свойство .notify ставиться, чтобы можно было отправлять уведомления, но для получения Central необходимо подписаться на них. Второе свойство .writeWithoutResponse означает, что запись в характеристику осуществляется без подтверждения.
var transferCharacteristic =
CBMutableCharacteristic(type: UUIDs.transferCharacteristicUUID,
properties:[.notify, .writeWithoutResponse],
value:nil,
permissions:[.readable, .writeable])
Теперь создаем сервис. Ставим primary=true, чтобы он был добавлен в перечень сервисов в advertising пакете.
var primaryService = CBMutableService(type:UUIDs.primaryServiceUUID, primary:true)
Добавляем характеристики.
primaryService.characteristics = [readCharacteristic, transferCharacteristic]
Созданные атрибуты передаем в CBPeripheralManager.
manager = CBPeripheralManager(delegate: self, queue: nil)
for service in gattProfile.services {
manager.add(service)
}
При создании CBPeripheralManager необходимо указать делегат CBPeripheralManagerDelegate. Если после инициализации peripheral manager функция
(void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
возвращает состояние peripheral.state = .poweredOn, значит manager готов к работе.
GATT профиль заполнен. Осталось собрать Advertising пакет и можно запускать передачу в радиоэфир. Структура пакета — это массив [String:Any] значений. Основной параметр, который нам нужно задать — идентификатор primary сервиса.
struct Advertisment {
var value = [CBAdvertisementDataServiceUUIDsKey:[UUIDs.primaryServiceUUID]]
}
Можно было бы еще добавить параметр CBAdvertisementDataLocalNameKey, но в данном случае это не имеет значения. Наши данные для advertising пакета объединяются с advertising данными смартфона. Этот параметр перезаписывается и как правило localName мы видим как iPhone 7.
Для запуска Advertising достаточно выполнить команду.
manager.startAdvertising(Advertisment().value)
Создаем профиль GATT клиент
Для реализации клиентского профиля нам потребуются классы: CBCentralManager, CBCentralManagerDelegate, CBPeripheralDelegate. Последний нужен для работы с найденным GATT сервером, т.к. получаем экземпляр CBPeripheral. Для того, чтобы найти сервер нам нужно создать экземпляр менеджера CBCentralManager и запустить поиск.
var manager = CBCentralManager(delegate: self, queue: nil)
manager.scanForPeripherals(withServices: [UUIDs.primaryServiceUUID],
options: managerOptions)
В качестве параметра withServices: передаем массив uuids — по этим идентификаторам будет осуществляться автоматическая фильтрация, будут возвращены только те peripherals, идентификаторы которых заданы в массиве. Передавая nil можно обнаружить все активные BLE устройства поблизости. В нашем случае был создан только один сервис, его uuid и передаем в качестве параметра. Параметр options: не обязателен, я передаю туда:
private let managerOptions = [CBCentralManagerScanOptionAllowDuplicatesKey:false,
CBCentralManagerOptionShowPowerAlertKey:false]
Первый параметр означает, что за время поиска мне вернется только первый обнаруженный Advertising пакет от конкретного устройства, остальные будут игнорироваться. Второй параметр означает, что выключаю системный диалог с предупреждением о состоянии bluetooth. Состояние будет приходить, а вот диалог не отобразится.
Если устройство с указанным uuid сервисом было обнаружено, то метод didDiscoverPeripheral:
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData
RSSI:(NSNumber *)RSSI;
вернет объект (CBPeripheral *)peripheral. Обязательно нужно сохранить на него ссылку.
var device:CBPeripheral = peripheral
Итак, после обнаружения устройства, мы можем подключиться к нему.
manager.connect(device, options: nil)
Подключением и отключением управляет CBCentralManager. Отключиться можно командой:
manager.cancelPeripheralConnection(cancelingDevice)
После успешного подключения делаем запрос на сервисы:
device?.discoverServices(nil)
В качестве параметра можем передать массив uuids сервисов, которые хотели бы прочитать. В данном случае стоит nil значит peripheral вернет все имеющиеся.
Чтение характеристик происходит схожим образом, только отправить запрос нужно индивидуального для каждого сервиса. Метод didDiscoverServices возвращает массив найденных сервисов и дальше, выбрав нужный делаем запрос на характеристики. Я не стал запрашивать для всех, т.к. у iPhone параллельно запущено еще несколько служебных, для понимания процесса это только запутает.
guard let services = device?.services else { return }
let filterServices = services.filter { (service) -> Bool in
service.uuid == UUIDs.primaryServiceUUID
}
for service in filterServices {
device?.discoverCharacteristics(nil, for: service)
}
Найденные характеристики возвращает метод didDiscoverCharacteristicsFor service:, т.е. последовательно для каждого сервиса. Необходимо подписаться на характеристику c идентификатором transferCharacteristicUUID.
if (characteristic.uuid == UUIDs.transferCharacteristicUUID) {
device?.setNotifyValue(true, for: characteristic)
}
Теперь можно передавать данные между созданными профилями GATT клиент/сервер.
Клиент отправляет данные таким образом:
device?.writeValue(text.data(using: .utf8)!,
for: transferCharacteristic,
type: .withoutResponse)
Принимает методом делегата CBPeripheralDelegate:
- (void)peripheral:(CBPeripheral *)peripheral
didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic
error:(nullable NSError *)error;
Подробнее можно посмотреть в коде репозитория, ссылка на проект в начале раздела.
MTU. Особенности передачи данных
Передавая данные между Central и Peripheral, важно понимать, что есть ограничение на одну порцию данных. За это отвечает параметр MTU. Перед отправкой нужно определить это значение. Central делает это так:
let mtu = device?.maximumWriteValueLength (for: .withoutResponse)
Если общий объем превышает MTU, то нужно разбить данные на соответствующие порции.
Для Peripheral MTU определяется так:
let mtu = connectedCentral?.maximumUpdateValueLength
Ссылку на подключенный Central нужно сохранить предварительно, когда делегат CBPeripheralManagerDelegate обрабатывает запрос подписи на характеристику.
- (void)peripheralManager:(CBPeripheralManager *)peripheral
central:(CBCentral *)central
didSubscribeToCharacteristic:(CBCharacteristic *)characteristic;
Передача данных между Central и Peripheral осуществляется без проверок, достаточно прогнать в цикле несколько раз запись в характеристику и все. Когда передаем наоборот от Peripheral к Central, то алгоритм гораздо хитрее. Вначале нужно разделить имеющиеся данные на порции, затем начать отправку. Причем функция updateValue может вернуть false.
func updateValue(_ value: Data,
for characteristic: CBMutableCharacteristic,
onSubscribedCentrals centrals: [CBCentral]?) -> Bool
Это означает, что в данный момент Peripheral еще не успел отправить предыдущую порцию данных и нужно немного подождать. О том, что характеристика вновь доступна для записи сигнализирует метод делегата CBPeripheralManagerDelegate:
- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral;
В приложении есть полный алгоритм передачи данных между Central и Peripheral. По нажатию кнопки «Send long text» отправляется строка чуть больше 4000 байт.
Что в итоге получилось
Изучив основные принципы коммуникаций между BLE устройствами мы, методом проб и ошибок, постарались разработать удобный интерфейс, в котором максимально сократили действия со стороны пользователя для входа в свое рабочее окружение. Теперь один раз требуется ввести учетные данные в приложение, а для последующих подключений только идентификатор GM-Box. Всю остальную работу по подключению, проверке и безопасному хранению учетных данных, приложение берет на себя. В результате, подходя к рабочему месту, пользователю достаточно нажать одну кнопку и можно приступить к работе.
Теперь предлагаю пройтись по основным возможностям нашего приложения.
Поиск устройств
Пару слов для понимания, что такое идентификатор GM-Box. Что бы как-то отличать одно устройство от другого мы придумали уникальный четырехзначный номер, который отображается на мониторе GM-Box. Для того, чтобы авторизоваться, пользователю необходимо ввести этот номер в приложении. После однократного ввода он сохраняется и отображается на кнопке.
Сразу после запуска приложения начинается фоновый поиск устройств, составляется реестр найденных. Операция длится около четырех секунд и проходит незаметно для пользователя.
Экран, который отображается ниже, пользователь увидит только в том случае, если за время поиска были найдены не все устройства или введен несуществующий идентификатор, приложение о нем ничего не знает, поэтому попробует найти.
На практике мы убедились, что вызов метода:
manager.scanForPeripherals(withServices: [UUIDs.primaryServiceUUID],
options: managerOptions)
Не всегда приводит к 100% обнаружению BLE устройств. Это и послужило основной причиной создания дополнительного экрана, где можно запустить поиск еще раз и попытаться найти устройство с запрошенным идентификатором.
Подключение к GM-Box
Если приложение только установлено, то после ввода идентификатора GM-Box, необходимо ввести свои учетные данные. С этого же экрана происходит переподключение, если данные были введены некорректно и проверка прошла с ошибкой.
Переход на экран отображающий статус, что приложение подключено, осуществляется после успешного завершения проверки учетных данных. Следует отметить, что учетные данные на GM-Box передаются в зашифрованном виде.
Завершение сессии
В любой момент пользователь может завершить сессию. Стандартный способ — вызвать диалог и подтвердить завершение.
А еще есть возможность настроить автоматический выход и спокойно уйти с рабочего места, не беспокоясь о сохранности данных. Сеанс завершится автоматически.
Защита приложения
Функционал не требует защищать доступ к приложению в обязательном порядке. По желанию, пользователь может сам установить ПИН-код, либо выбрать вход используя биометрию. После 10-ти попыток неверно введенного значения ПИН-кода, учетные данные стираются.
Послесловие
Подробнее с функционалом приложения можно ознакомиться на AppStore или Google Play.
Дальнейшее развитие приложения будет связано с добавлением новых возможностей. В том числе, планируется ввести автоматический вход в рабочую сессию при поднесении смартфона на достаточно близкое расстояние к GM-Box. Также, по результатам проведенных пилотов, некоторые заказчики хотели бы получить функционал не использующий алгоритм хранения учётных данных на телефоне, несмотря на все предпринятые меры по обеспечению безопасности хранения и передачи данных на смартфонах. Кроме этого, анализ обратной связи от пользователей показал, что была бы удобна функция быстрого блокирования рабочей сессии, если сотруднику
необходимо отойти или переключиться на разговор с коллегой — эту опцию мы тоже рассматриваем.
aamonster
А, то есть это не автоматическая разблокировка/блокировка лежащим в кармане телефоном, а для входа нужно запустить приложение и указать конкретную рабочую станцию?
Тогда почему забыли про такой вариант, как сканирование смартфоном qr-code на экране?
swiftSoul Автор
Разные варианты рассматривали. У нас приложение может читать идентификатор по NFC. Этого не написано в статье.
aamonster
А чтения идентификатора по nfc недостаточно, чтобы залогиниться через сеть, не устанавливая прямого bluetooth-соединения?
Ну то есть получили идентификатор, послали запрос через internet/intranet (подтвердив эцп на смартфоне). Собственно, тот же сценарий, что вход по сканированию qr-кода.
swiftSoul Автор
Все верно, NFC достаточно для чтения идентификатора, но он исторически появился позже и не всегда стабильно работает на разных моделях телефонов. Небольшой глюк и пользователю будет неудобно пользоваться. Поэтому решили оставить оба варианта: ручной ввод и NFC. Между QR и NFC разница не большая. Да и после ввода, идентификатор сохраниться, появиться кнопка с номер, которую будет достаточно нажать для подключения.
Причины по которым не стали использовать WIFI на телефоне: не во всех организациях разрешено использование, bluetooth безопаснее. Вообще варианты ориентированные на сеть рассматриваются.