Глава 4. Android-терминал: Интерфейс без памяти
2026-02-23
Аннотация В данной главе рассматривается техническая реализация Android-клиента для аппаратно-программного комплекса шифрования GuardianT. В отличие от классических мессенджеров, приложение функционирует в режиме «терминала», делегируя операции с приватными ключами внешнему аппаратному модулю (ESP8266/ESP32). Мы подробно разберем стек технологий, включая протоколы обнаружения устройств, встроенные HTTP-серверы и гибридную криптографию.
4.1. Введение: Смартфон как «Недоверенная среда»
Современные мобильные операционные системы (Android/iOS) обеспечивают удобство, но не приватность. Система имеет доступ к буферу обмена, клавиатурному вводу и содержимому экрана.
Проблема: Если ОС скомпрометирована, программное шифрование бесполезно — данные будут перехвачены до момента шифрования. Решение: Архитектура GuardianT выносит криптографическое ядро в физически изолированное устройство. Android-приложение становится лишь интерфейсом ввода-вывода (I/O), не хранящим секретов.
4.2. Архитектура приложения
Приложение написано на языке Kotlin с использованием паттерна MVVM (Model-View-ViewModel). Это позволяет отделить логику работы с данными от пользовательского интерфейса.
Стек технологий:
- Kotlin & Coroutines: Для асинхронной работы (сеть, I/O).
- ViewBinding: Для безопасного доступа к элементам интерфейса.
- NanoHTTPD: Встраиваемый легковесный веб-сервер.
- Java Cryptography Architecture (JCA): Для локального шифрования файлов.
4.3. Техническая реализация: Шаг за шагом
4.3.1. Поиск оборудования: Протокол UDP Discovery
Аппаратный ключ (ESP8266) при подключении к Wi-Fi получает динамический IP-адрес. Чтобы приложение могло найти ключ без ручного ввода IP, мы используем протокол UDP Broadcast.
Как это работает: Приложение «кричит» в сеть специальную фразу. Все устройства слышат её, но отвечает только то, которое знает пароль.
Реализация (Kotlin):
Мы используем класс DatagramSocket. Важно выполнять это в отдельном потоке (Thread или Coroutine), так как Android запрещает сетевые операции в главном потоке.
/* Файл: EspDiscovery.kt */
fun discoverDevice(onFound: (String) -> Unit) {
Thread {
try {
// 1. Создаем сокет для отправки
val socket = DatagramSocket()
socket.broadcast = true // Разрешаем широковещание
val message = "DISCOVER_GARDIANT".toByteArray()
// 2. Отправляем пакет на адрес 255.255.255.255
// Это специальный адрес: "всем в этой сети"
val packet = DatagramPacket(
message, message.size,
InetAddress.getByName("255.255.255.255"), 4211
)
socket.send(packet)
// 3. Ждем ответ
val buffer = ByteArray(1024)
val responsePacket = DatagramPacket(buffer, buffer.size)
socket.soTimeout = 2000 // Ждем не более 2 секунд
socket.receive(responsePacket) // Блокирующее ожидание
val responseData = String(responsePacket.data, 0, responsePacket.length)
if (responseData.contains("GARDIANT_HERE")) {
// Устройство найдено! Извлекаем его IP
val deviceIp = responsePacket.address.hostAddress
onFound(deviceIp)
}
socket.close()
} catch (e: Exception) {
e.printStackTrace()
}
}.start()
}
4.3.2. Обратная связь: Встроенный HTTP-сервер (NanoHTTPD)
Для первичной настройки (передачи пароля от Wi-Fi на устройство) мы меняем роли. Телефон становится сервером, а ESP8266 — клиентом. Для этого используется библиотека NanoHTTPD.
Зачем это нужно? Это позволяет передать сложные настройки (SSID, пароль, URL сервера) на устройство через простой HTTP GET запрос, который устройство делает к телефону.
Реализация:
/* Файл: ProvisioningServer.kt */
// Наследуемся от NanoHTTPD
class ProvisioningServer(port: Int) : NanoHTTPD(port) {
override fun serve(session: IHTTPSession): Response {
// Проверяем, какой URL запросило устройство
if (session.uri == "/config") {
// Формируем JSON с настройками
val jsonConfig = """
{
"wifi_ssid": "MyHomeNetwork",
"wifi_pass": "SecretPassword123",
"server_url": "https://chat.my-server.com"
}
""".trimIndent()
return newFixedLengthResponse(
Response.Status.OK,
"application/json",
jsonConfig
)
}
return newFixedLengthResponse("Not Found")
}
}
4.3.3. Гибридная криптография: AES-256
Аппаратный ключ (ESP8266) имеет мало памяти, поэтому он шифрует только текстовые сообщения. Большие файлы (фото, документы) шифруются на телефоне, но ключ шифрования генерируется случайно для каждого файла и никогда не сохраняется.
Мы используем алгоритм AES в режиме CBC (Cipher Block Chaining).
Важный нюанс: Для каждого файла обязательно генерируется уникальный вектор инициализации (IV). Если зашифровать один и тот же файл одним ключом, но разными IV — результат будет абсолютно разным. Это защищает от статистического анализа.
/* Файл: CryptoUtils.kt */
object CryptoUtils {
fun encryptFile(data: ByteArray, secretKey: String): ByteArray {
// 1. Подготовка ключа
val keyBytes = secretKey.toByteArray().copyOf(32) // 256 бит
val keySpec = SecretKeySpec(keyBytes, "AES")
// 2. Генерация случайного IV (16 байт)
val iv = ByteArray(16)
SecureRandom().nextBytes(iv)
val ivSpec = IvParameterSpec(iv)
// 3. Настройка шифра
val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding")
cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec)
// 4. Шифрование
val encryptedData = cipher.doFinal(data)
// 5. Упаковка: Сначала кладем IV, потом данные
// Получатель считает первые 16 байт как IV
return iv + encryptedData
}
}
4.3.4. Взаимодействие с API (HTTP Client)
Приложение общается с аппаратным ключом через REST API. Мы используем стандартный HttpURLConnection для обеспечения прозрачности и отсутствия лишних зависимостей.
/* Файл: HardwareClient.kt */
fun sendTextToHardware(ip: String, text: String) {
Thread {
val url = URL("http://$ip/encrypt?msg=$text")
with(url.openConnection() as HttpURLConnection) {
requestMethod = "GET"
// Читаем ответ от устройства (это будет зашифрованный блоб)
inputStream.bufferedReader().use {
val encryptedBlob = it.readText()
// Далее этот блоб отправляется на внешний сервер
sendToServer(encryptedBlob)
}
}
}.start()
}
4.4. Безопасность пользовательского интерфейса (UI Security)
Даже самая надежная криптография бессильна, если вредоносное приложение сделает скриншот вашего экрана. Android предоставляет механизм защиты FLAG_SECURE.
Реализация:
В методе onCreate главной активности (MainActivity) мы устанавливаем флаг, который запрещает системе и другим приложениям видеть содержимое окна.
/* Файл: MainActivity.kt */
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Блокировка скриншотов и предпросмотра в меню "Недавние"
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
)
// Инициализация UI через ViewBinding
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
}
Очистка буфера обмена
Приложение автоматически очищает буфер обмена при сворачивании, чтобы скопированные сообщения не остались в памяти телефона.
4.4. Работа с сетью
Приложение общается с сервером через REST API.
interface GuardianApiService {
// Отправка зашифрованного пакета
@POST("/drop")
suspend fun dropMessage(@Body packet: EncryptedPacket): Response<DropResult>
// Получение сообщений
@GET("/pickup")
suspend fun pickupMessages(@Query("device_id") deviceId: String): Response<List<EncryptedPacket>>
}
Обратите внимание: в API нет методов "login" или "register" в привычном понимании. Аутентификация происходит на уровне подписи пакетов аппаратным ключом.
Итог
Android-приложение GuardianT — это лишь удобный пульт управления. Если вы потеряете телефон, вы не потеряете свою личность, так как ключи остались в аппаратном модуле.