Небольшое предисловие
Привет, Хабр!
Данная статья является четвертой в цикле “Диплом специалиста ИБ”, в рамках которого я рассказываю про свой опыт написания выпускной квалификационной работы на программе высшего образования “Компьютерная безопасность”. В предыдущей статье я описывал процесс разработки портативного устройства IoT “SmartPulse” с реализацией механизмов защиты из методики обеспечения безопасности устройств Интернета вещей, которая была предложена мной в первой части данного цикла статей. В текущей статье речь пойдет про создание мобильного приложения Smart Connect, с помощью которого я реализовал управление разработанными ранее устройствами Интернета вещей.
Если вы не успели ознакомиться с предыдущими статьями данного цикла, советую сначала прочитать их для того, чтобы лучше понимать контекст происходящего.
Мобильное приложение Smart Connect
После того, как были разработаны и собраны два устройства Интернета вещей: SmartLight без реализации механизмов защиты и SmartPulse с реализацией данных механизмов, возникла потребность создания приложения, с помощью которого можно было бы наиболее комфортным образом взаимодействовать с ними.
Так как мне раньше не приходилось писать какой-либо софт под мобильные платформы, я решил остановиться именно на мобильном приложении. Мне хотелось, чтобы в рамках написания выпускной квалификационной работы, я смог приобрести новый опыт, а не просто сделать “на отвали” ради оценки. Поэтому я понял, что буду пытаться разобраться в мобильной разработке, чтобы этот новый опыт получить. Ну и на мой взгляд, с устройствами Интернета вещей наилучшим и наиболее логичным образом взаимодействовать именно посредством смартфона.
Концепт мобильного приложения
Идея мобильного приложения Smart Connect в моем понимании должна была заключаться в раздельном управлении устройствами. Мне бы хотелось проработать концепцию дашборда с отображением информации по всем устройствам на одном экране, но в рамках дипломной работы я ограничился двумя отдельными профилями. То есть, сначала на главном экране мобильного приложения должны быть отображены устройства SmartLight и SmartPulse, а затем пользователь может перейти в панель управления нужного ему устройства для дальнейшего взаимодействия с ним.
Язык программирования и фреймворк
Когда мной было принято решение писать мобильное приложение, я начал выбирать то, с чем хотел бы больше поработать. В основном выбор был между React Native или Flutter. Swift я не рассматривал в принципе из-за узкой направленности на устройства Apple (хотя в будущем думаю чуть ближе с ним ознакомиться). Знаю, что выбор звучит как-то достаточно смешно, но по факту это не так, потому что на тот момент у меня был опыт работы с React.js. Так как я никогда не пробовал свои силы в мобильной разработке, React Native мне виделся чем-то более привычным и знакомым.
В итоге я все-таки выбрал Flutter и, соответственно, язык программирования Dart. Мой выбор обусловлен наибольшей популярностью Flutter, что в итоге все же взяло верх надо мной. Могу сказать, что я совершенно не пожалел о своем решении.
Операционная система и другие тонкости
Так вышло, что я являюсь владельцем iPhone 12 Pro Max, поэтому у меня не оставалось другого выбора, кроме как писать мобильное приложение под операционную систему iOS 17.1.2.
Для того, чтобы собрать и запустить мобильное приложение Flutter на устройстве с операционной системой iOS, необходимо установить Xcode. Помимо официальной среды разработки, также нужен аккаунт разработчика Apple Developer.
Чтобы работать с физическим устройством и устанавливать все сборки приложения непосредственно на iPhone, я перевел его в режим разработчика.
К разрабатываемому приложению необходимо сгенерировать Bundle ID, но в этом ничего сложного нет. Главное помнить, что не стоит генерировать новый идентификатор после каждой новой сборки (если у кого-то зачем-то возникает желание так делать), так как Apple выдает ограниченное количество ID на одного разработчика в течение недели.
Больше никаких предварительных процедур для разработки под iOS мне не потребовалось. Про установку Flutter, настройку окружения и тд писать не вижу смысла. Оставляю ссылку на официальную документацию Flutter с разработкой под iOS.
Код приложения SmartConnect
По уже устоявшейся традиции код целиком можно увидеть в репозитории на GitHub, а также в приложениях к моей ВКР. В рамках статьи разберем наиболее интересные фрагменты. Привычным образом разобьем весь код на структурные блоки.
Структурный блок | Назначение и функционал |
---|---|
Библиотеки | Импорт библиотек |
Класс MyApp | Основной класс приложения, наследующий StatelessWidget. Создание UI приложения с темной темой и начальным экраном FindDevicesScreen |
Класс FindDevicesScreen | Отображение и сканирование доступных Bluetooth-устройств. Включает AppBar, RefreshIndicator и StreamBuilder |
Переход к экранам управления устройствами | Логика для перехода на экраны устройств (DeviceScreen, SmartPulseDeviceScreen) в зависимости от выбора пользователя |
Класс DeviceScreen | Класс для взаимодействия с SmartLight |
Класс SmartPulseDeviceScreen | Класс для взаимодействия с SmartPulse |
Управление Состоянием и Данными | Функции для отправки команд и чтения данных с устройств |
Виджеты | Определение виджетов для отображения информации, текстовых полей и кнопок |
Для реализации передачи данных по BLE я использовал flutter_blue
, которую изначально необходимо прописать в pubspec.yaml
. Здесь же сразу добавляем flutter_launcher_icons
, чтобы в дальнейшем заменить дефолтную иконку приложения Flutter на свою.
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.2
flutter_blue: ^0.8.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^2.0.0
flutter_launcher_icons: ^0.13.1
Далее, после обновления всех зависимостей через flutter pub get
, можно приступать к написанию основного кода мобильного приложения.
Так как мобильное приложение Smart Connect необходимо исключительно для взаимодействия с ранее разработанными SmartLight и SmartPulse, во время сканирования BLE-серверов мобильным устройством, искусственно ограничиваем вывод перечня доступных для подключения устройств до двух конкретных. В разработке каждого из данных устройств я в явном виде указывал наименование BLE-сервера, поэтому мы можем реализовать данный процесс просто по названию. Хотя в случае проектирования коммерческих устройств я бы не советовал так делать из-за возможных проблем, связанных с одинаковыми именами профилей устройств.
// Обработчик нажатия на элемент списка
onTap: () {
if (device.device.name == 'SmartLight') {
// Осуществляем переход на соответствующий экран
Navigator.of(context).push(MaterialPageRoute(
// Переход на экран DeviceScreen
builder: (context) =>
DeviceScreen(device: device.device),
));
} else if (device.device.name == 'SmartPulse') {
Navigator.of(context).push(MaterialPageRoute(
// Переход на экран SmartPulseDeviceScreen
builder: (context) =>
SmartPulseDeviceScreen(device: device.device),
));
}
}
Для взаимодействия с устройством SmartPulse я создал отдельный класс, в котором отделил логику управления устройством от всего остального кода.
В нем можно видеть процесс чтения значения характеристики датчика пульса. Раз в секунду считывается новое значение характеристики, идентификатор которой указан в явном виде. Затем оно записывается в переменную pulseRate
. В дальнейшем значение этой переменной будем выводить на экран смартфона.
// Создание подписки на поток данных о пульсе
_pulserateSubscription =
Stream.periodic(Duration(seconds: 1)).asyncMap((_) async {
// Чтение характеристики устройства (пульс)
List<int> value = await readCharacteristic(
widget.device, Guid('0000fef4-0000-1000-8000-00805f9b34fb'));
int pulseRate = (value[0]);
return pulseRate;
}).listen((pulseRate) {
// Обновление состояния виджета с новым значением пульса
setState(() {
this.pulseRate = pulseRate;
});
});
Для того, чтобы в принципе был осуществим процесс чтения содержимого какой-либо BLE-характеристики, создаем асинхронную операцию.
// Метод для чтения характеристики
Future<List<int>> readCharacteristic(
BluetoothDevice device, Guid characteristicGuid) async {
// Получение списка сервисов Bluetooth устройства
List<BluetoothService> services = await device.discoverServices();
// Перебор сервисов и их характеристик
for (BluetoothService service in services) {
for (BluetoothCharacteristic characteristic in service.characteristics) {
// Проверка соответствия UUID характеристики
if (characteristic.uuid == characteristicGuid) {
// Чтение значения характеристики
List<int> value = await characteristic.read();
return value;
}
}
}
// Вывод исключения, если характеристика не найдена
throw Exception('Characteristic not found: $characteristicGuid');
}
Аналогичным образом нужно было поступить и для реализации функционала перезаписи значения BLE-характеристики, так как и в случае взаимодействия с устройством SmartPulse, и в рамках управления SmartLight, должна быть реализована возможность передачи собственных значений. В пульсометре данный функционал завязан на записи аутентификационных данных в виде пин-кода в одну из характеристик, а в светильнике таким образом реализован функционал переключения режимов работы устройства, а также все функции в ручном режиме. Если говорить про сам код, то логика тут такая же, как и в случае чтения значений характеристики. Схожесть кода как структурно, так и логически заметна невооруженным глазом.
// Метод для записи в характеристику
Future<void> writeCharacteristic(
BluetoothDevice device, Guid characteristicGuid, List<int> value) async {
List<BluetoothService> services = await device.discoverServices();
// Перебор сервисов и их характеристик
for (BluetoothService service in services) {
for (BluetoothCharacteristic characteristic in service.characteristics) {
if (characteristic.uuid == characteristicGuid) {
// Запись значения в характеристику
await characteristic.write(value);
return;
}
}
}
}
Так как я продемонстрировал процесс чтения значения характеристики, то покажу и запись в характеристику.
// Метод для отправки сообщения
Future<void> sendTextMessage() async {
// Получение значения из текстового поля
String text = textController.text;
// Кодирование текста (UTF-8)
List<int> value = utf8.encode(text);
try {
// Попытка записи в характеристику
await writeCharacteristic(
widget.device,
Guid('0000fef3-0000-1000-8000-00805f9b34fb'),
value,
);
} catch (e) {
// Вывод ошибки
print("Error writing to characteristic: $e");
}
}
Так выглядит передача аутентификационного пин-кода пользователя на устройство SmartPulse. После того, как пользователь запишет значение пин-кода в характеристику с указанным идентификатором, в прошивке устройства сработает скрипт проверки данного значения. Если совпадение будет найдено, то устройство запустит рассылку пакетов с характеристиками датчика пульса и заряда аккумулятора на подключенное устройство. В приложении они также отобразятся и будут выведены на экран в соответствующих полях.
После того, как получилось обеспечить возможность работы с BLE, дело было за малым — дать пользователю возможность видеть значения характеристик и передавать свои собственные. Создаем страницу устройства SmartPulse, на которую мы сможем перейти при подключении к нему. В соответствующих полях будем выводить значения переменных пульса и заряда аккумулятора, которые изначально получили из характеристик при подключении к устройству. Также оставляем текстовое поле для возможности записи в него аутентификационного пин-кода, который будет отправлен по кнопке, в обработчике события которой прописываем вызов sendTextMessage()
. Данную операцию мы уже рассматривали выше; она реализует процесс записи значения в необходимую характеристику.
@override
Widget build(BuildContext context) {
// Использование виджета Scaffold для создания базовой структуры экрана
return Scaffold(
// AppBar для отображения заголовка экрана
appBar: AppBar(
title: Text('Управление SmartPulse'),
),
// Основное содержимое экрана, размещенное по центру
body: Center(
child: Column(
// Выравнивание элементов колонки
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// Отображение текущего пульса
Text('Пульс: $pulseRate', style: TextStyle(fontSize: 24)),
// Отображение текущего уровня заряда батареи
Text('Заряд аккумулятора: $batteryLevel%',
style: TextStyle(fontSize: 24)),
// Поле для ввода текста
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
// Контроллер для управления текстовым полем
controller: textController,
decoration: InputDecoration(
border: OutlineInputBorder(),
labelText: 'Пароль для аутентификации',
),
),
),
// Кнопка для отправки текстового сообщения
ElevatedButton(
onPressed: () {
sendTextMessage();
},
child: Text('Подтвердить'),
),
// Кнопка для отключения от устройства
ElevatedButton(
onPressed: isDeviceConnected ? disconnectDevice : null,
child: Text('Отключиться от устройства'),
),
],
),
),
);
}
}
Для SmartLight все реализовано по той же логике, что и для SmartPulse. В его функционале подразумевается работа с большим числом характеристик, поэтому код получился значительно объемнее, чем в случае работы с пульсометром, но по факту структурно в нем нет ничего отличного от продемонстрированных выше фрагментов кода.
Чтобы было возможным использование BLE при запуске и работе мобильного приложения, необходимо открыть файл Runner.xcworkspace
Flutter-проекта, который находится в папке ios
, в Xcode. В открывшемся проекте в среде разработки переходим в info.plist
и добавляем строки со значениями "NSBluetoothPeripheralUsageDescription"
.
Также прошу прощения у всех опытных и знающих разработчиков мобильных приложений и кому было больно смотреть на мой код. Это был мой первый опыт работы с Flutter, прошу строго меня за это не судить.
Интерфейс мобильного приложения
По сути, приложение состоит из главной страницы, на которой размещаются "плитки" устройств, к которым можно подключиться. То есть, после запуска приложения происходит сканирование всех BLE-устройств поблизости и поиск двух наших устройств по наименованию BLE-сервера (никогда так не делайте). Если находим какое-либо из устройств, то выводим плитку с ним на главный экран, чтобы можно было перейти на экран управления устройством и, собственно, взаимодействовать с ним.
Меню управления SmartLight
Интерфейс меню управления SmartLight включает в себя переключение между режимами работы устройства. При включении автоматического режима (который установлен по умолчанию при подключении к устройству), ручной режим блокируется, и аналогичная ситуация происходит при переключении на ручной режим управления.
В автоматическом режиме я оставил только кнопку переключения на ручной режим, а также вывод информации с датчика освещенности.
В ручном режиме реализовано включение светового элемента на SmartLight с помощью кнопки "Белый свет" и включение цветового динамического градиента по кнопке "Градиент". Универсальная кнопка "Выключить" нужна для выключения светового элемента устройства вне зависимости от запущенного на нем эффекта.
Для включения и выключения электрохромной пленки я решил использовать переключатель (toggle switch), который также работает при любом запущенном эффекте на светодиодной матрице устройства.
Для отправки на устройство пользовательского сообщения существует текстовое поле и кнопка "Отправить сообщение". После ввода сообщения и нажатия на данную кнопку будет запущен сценарий устройства, при котором электрохромная пленка автоматически переключится на прозрачный режим, а пользовательское сообщение будет передаваться на светодиодную матрицу устройства в виде бегущей строки. После окончания отображения сообщения, устройство самостоятельно выключит электрохромную пленку и перейдет в режим ожидания следующей команды со стороны пользователя.
Кнопка "Отключиться от устройства" просто разрывает соединение со SmartLight. При этом устройство все равно будет доступно для подключения к нему через плитку на главном экране приложения.
Меню управления SmartPulse
Для взаимодействия с пульсометром SmartPulse я разместил поле для ввода аутентификационного пин-кода, который поступит на устройство в виде записи в отдельную BLE-характеристику. После подтверждения корректности пин-кода на стороне устройства в соответствующих полях на экране управления отобразятся значения пульса и уровня заряда аккумулятора портативного устройства. По кнопке "Отключиться от устройства" происходит разрыв соединения с пульсометром. При этом в данном случае повторно подключиться к устройству получится только после физической перезагрузки SmartPulse, что было сделано в рамках ограничения количества подключений к устройству в качестве одного из механизмов защиты из методики обеспечения безопасности, описанной в первой части цикла статей.
Демонстрация устройств и приложения
Для наглядной демонстрации взаимодействия с устройствами Интернета вещей посредством мобильного приложения я отснял и смонтировал два видео со всем реализованным функционалом.
Чтобы в процессе просмотра видео не возникало вопросов из разряда "Что тут происходит?", я решил добавить тайм-коды (они ведут на соответствующие видео на YouTube) с подробным описанием и пояснением всего, что происходит у вас на экране. Начнем с устройства SmartLight.
Устройство SmartLight
00:03-00:39 Сценарий приветствия устройства SmartLight при его включении. Происходит автоматическое включение электрохромной пленки для того, чтобы можно было прочитать приветствие, которое отображается на устройстве в виде бегущей строки. После завершения сценария, устройство выключает пленку и переходит в автоматический режим работы.
00:40-00:59 Процесс подключения к устройству начинается при открытии приложения Smart Connect, когда можно увидеть главный экран пустым. Для отображения на нем доступных для подключения устройств нужно запустить сканирование, что делается по кнопке в нижнем углу экрана. После завершения сканирования отображается устройство SmartLight, по плитке с названием которого можно перейти на экран управления устройством. При подключении к устройству на SmartLight светодиод-индикатор подключения по BLE сменит цвет свечения с синего на зеленый, что можно увидеть в видео, если немного присмотреться. Так как устройство по умолчанию переведено в автоматический режим работы, можно наблюдать, что в приложение сразу после подключения было передано значение уровня освещенности (87).
01:00-01:05 Включение светодиодной матрицы устройства при недостаточной освещенности в автоматическом режиме работы. Значения уровня освещенности также упали с 87 до 50, что видно в приложении.
01:06-01:20 Переключение режима работы устройства с автоматического на ручной и включение/выключение светодиодной матрицы по кнопке в приложении.
01:21-01:38 Запуск эффекта градиентного свечения на устройстве.
01:39-01:53 Включение и выключение электрохромной пленки с помощью приложения.
01:54-02:25 Передача пользовательского сообщения на светодиодную матрицу устройства в виде бегущей строки. В текстовое поле на экране мобильного приложения пишем сообщение "Привет, Хабр!" и отправляем его по кнопке на устройство. Срабатывает похожий на приветствие сценарий с автоматическим включением и выключением электрохромной пленки.
02:26-02:30 Разрыв соединение с устройством по кнопке в приложении.
Устройство SmartPulse
Для демонстрации работы приложения с пульсометром я вновь подключил Аню (@ShabrovaAS), потому что так было удобнее снимать, а она не особо этому сопротивлялась.
00:04-00:14 Включение устройства SmartPulse. При загрузке устройства был сгенерирован пин-код "298980" для аутентификации в приложении.
00:15-00:19 Определение пульса и заряда аккумулятора с выводом информации на OLED дисплей устройства по прикосновению к touch сенсору.
00:20-00:30 Подключение к устройству с помощью мобильного приложения. Тут все аналогично тому же этапу в демонстрации SmartLight. Информация в приложении по пульсу и уровню заряда не отображается, так как еще не был введен аутентификационный пин-код.
00:31-00:49 Аутентификация по сгенерированному пин-коду. Вводим в соответствующее поле пин-код "298980" и записываем его в характеристику с помощью кнопки. После проверки на корректность происходит запуск рассылки сервисов с характеристиками пульса и заряда аккумулятора, значение которых отображаются на экране управления SmartPulse в приложении.
00:50-00:54 Отключение от устройства. На экране ничего не очищается (надо было сделать, но я не сделал), но новые значения более не отображаются в приложении. Если присмотреться к дисплею устройства, можно увидеть, что пульс изменился, но после отключения от SmartPulse в приложении, новое значение уже не было передано на смартфон.
В следующей части данного цикла статей будет рассмотрен процесс получения несанкционированного доступа к ранее созданными IoT-устройствами SmartLight и SmartPulse, а также подведены итоги защиты и написания моей дипломной работы.
Я благодарен всем, кто уделил время для прочтения данной статьи. Был бы безумно рад получить фидбек в виде комментариев, чтобы улучшать качество материала.
Что дальше?
Так как дипломная работа получилась достаточно объемной, я решил разбить ее на несколько связанных статей:
Диплом специалиста ИБ. Часть №1 - Методика обеспечения безопасности устройств Интернета вещей
Диплом специалиста ИБ. Часть №2 - Стационарное устройство SmartLight
Диплом специалиста ИБ. Часть №3 - Портативное устройство SmartPulse
Диплом специалиста ИБ. Часть №4 - Мобильное приложение Smart Connect
Диплом специалиста ИБ. Часть №5 - Несанкционированный доступ к IoT-устройствам с BLE
С оригиналом ВКР можно ознакомиться тут