Cypher
Модуль cypher предоставляет шифрование и дешифрование RTEA и AES.
Использует существующий алгоритм RTEA из OTA и реализацию AES из mbedTLS.
Строки Lua могут содержать произвольные бинарные данные, включая нулевые байты.
Модуль включается параметром CONFIG_LUA_RTOS_LUA_USE_CYPHER.
При включении Tinyweb он подключается автоматически для обмена настройками с web-UI.
local cypher = require("cypher")
Алгоритмы и режимы
| algorithm | Размер ключа | Размер IV | Длина данных |
|---|---|---|---|
rtea-ecb | 32 байта | не используется | кратна 8 байтам |
rtea-cbc | 32 байта | 8 байт | кратна 8 байтам |
aes-ecb | 16, 24 или 32 байта | не используется | кратна 16 байтам |
aes-cbc | 16, 24 или 32 байта | 16 байт | кратна 16 байтам |
aes-ctr | 16, 24 или 32 байта | 16 байт | произвольная |
Ключи и IV передаются бинарными строками, а не строками с шестнадцатеричными
цифрами. Например, ключ AES-128 содержит 16 байтов; его запись в hex содержит
32 символа. Для преобразования можно использовать utils.unhexlify(hex, true).
RTEA использует 64 раунда, блок из двух 32-битных слов и ключ из восьми
32-битных слов. Порядок байтов в блоке и словах ключа — little-endian.
Алгоритм совпадает с используемым для OTA, но ключ передачи настроек отдельный.
Утилита OTA принимает ключ как восемь числовых hex-слов; для cypher каждое
такое слово нужно представить четырьмя байтами little-endian.
cypher.encrypt(algorithm, data, key [, iv])
Шифрует данные выбранным алгоритмом.
Аргументы:
algorithm: имя алгоритма и режима из таблицы выше.data: строка с исходными данными.key: бинарная строка с ключом.iv: бинарный вектор инициализации для CBC или начальный счётчик для CTR. Для ECB аргумент не передаётся либо равенnil.
Возвращает: строка с зашифрованными данными той же длины, что и data.
Модуль не добавляет дополнение, IV или тег аутентификации к результату. Входные строки, включая IV, не изменяются. Пустая строка поддерживается. При неизвестном алгоритме, неверной длине данных, ключа или IV возникает ошибка Lua.
local cypher = require("cypher")
local key = utils.unhexlify("000102030405060708090a0b0c0d0e0f", true)
local data = utils.unhexlify("00112233445566778899aabbccddeeff", true)
-- Тестовый вектор AES-128 ECB. Фиксированный ключ здесь только для примера.
local encrypted = cypher.encrypt("aes-ecb", data, key)
local expected = utils.unhexlify("69c4e0d86a7b0430d8cdb78070b4c55a", true)
assert(encrypted == expected)
cypher.decrypt(algorithm, data, key [, iv])
Расшифровывает данные. Аргументы совпадают с cypher.encrypt, но data
содержит шифротекст. Нужно передать тот же ключ и исходный IV.
Возвращает: строка с расшифрованными данными.
Дополнение не удаляется. Неправильный ключ сам по себе не вызывает ошибку: эти режимы не проверяют подлинность сообщения.
local cypher = require("cypher")
local key = string.rep("k", 32) -- Тестовый ключ, не для рабочих устройств.
local iv = cypher.random(8)
local data = "12345678ABCDEFGH" -- Два блока RTEA.
local encrypted = cypher.encrypt("rtea-cbc", data, key, iv)
local decrypted = cypher.decrypt("rtea-cbc", encrypted, key, iv)
assert(decrypted == data)
CTR допускает неполный последний блок:
local cypher = require("cypher")
local key = string.rep("k", 32) -- Тестовый ключ AES-256.
local iv = cypher.random(16)
local data = "Произвольная строка"
local encrypted = cypher.encrypt("aes-ctr", data, key, iv)
assert(cypher.decrypt("aes-ctr", encrypted, key, iv) == data)
Каждый вызов CTR начинается с переданного счётчика; состояние между вызовами не сохраняется. Для разных сообщений с одним ключом используйте разные IV.
cypher.random(size)
Возвращает байты из генератора ESP32 через esp_fill_random().
Аргументы:
size: целое неотрицательное количество байтов.
Возвращает: бинарная строка длиной size. При size = 0 возвращается пустая строка.
Отрицательное значение или нехватка памяти вызывает ошибку Lua.
local iv = cypher.random(16)
assert(#iv == 16)
Качество энтропии определяется источниками, включёнными в ESP-IDF на устройстве. Обёртка не включает Wi-Fi, Bluetooth или дополнительные источники энтропии.
Аппаратное ускорение AES
AES доступен при CONFIG_MBEDTLS_AES_C=y. Для CBC и CTR также требуется
соответствующий режим mbedTLS. Если AES или режим отключён, вызов завершится
ошибкой Lua; RTEA остаётся доступным.
При CONFIG_MBEDTLS_HARDWARE_AES=y mbedTLS использует аппаратный AES ESP32.
Обёртка сохраняет штатную работу с контекстом, ключом и блокировкой ускорителя.
Отдельное API для доступа к регистрам не требуется.
Передача настроек web-UI
Все профили в components/fs_images используют формат rtea-cbc-v1:
- Таблица настроек преобразуется в JSON UTF-8.
- Данные дополняются PKCS#7 до границы 8 байтов.
- Создаётся новый 8-байтный IV и выполняется шифрование RTEA CBC.
- IV и шифротекст объединяются, преобразуются в hex и помещаются в JSON:
{"encoding":"rtea-cbc-v1","data":"<IV и шифротекст в hex>"}
GET /backend?action=settingsRead возвращает этот контейнер, включая значения
net_password, net_ap_password и sys_pwd. Запись выполняется методом
POST /backend?action=settingsWrite с таким же контейнером в теле запроса.
Ответ — ok; с параметром return=settings возвращается контейнер с актуальными
настройками. Метод PUT сохранён как совместимый вариант записи.
Lua-модуль settings_wire.decode(body) возвращает таблицу настроек либо вызывает
ошибку Lua. Он проверяет версию контейнера, строковый тип data, дополнение
PKCS#7 и то, что расшифрованный JSON начинается с объекта {, а не массива
или скалярного значения. Проверку hex выполняет utils.unhexlify, длины IV
и блоков — cypher.decrypt, синтаксиса JSON — cjson.decode.
Backend вызывает декодер через pcall. При неверном контейнере, шифротексте,
дополнении или JSON запрос получает error: invalid encoded settings до записи
настроек в NVS. Ошибка декодирования не выходит за пределы обработчика запроса.
При отдельном использовании декодера для недоверенных данных также нужен pcall.
В профиле eth-rs485-modbus тот же формат используется для action=config,
включая пароль MQTT. Открытый JSON настроек при записи отклоняется. При обновлении
нужно заменить и прошивку/ROMFS, и клиентские web-файлы.
Это обфускация с публичным ключом, доступным браузеру. Она убирает открытый JSON из обмена, но не заменяет HTTPS и web-пароль, не подтверждает подлинность сообщения и не предотвращает повторную отправку перехваченного запроса. Формат хранения NVS не меняется. В диагностических выгрузках пароли по-прежнему скрываются.