Перейти к содержанию
Материалы раздела

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-ecb32 байтане используетсякратна 8 байтам
rtea-cbc32 байта8 байткратна 8 байтам
aes-ecb16, 24 или 32 байтане используетсякратна 16 байтам
aes-cbc16, 24 или 32 байта16 байткратна 16 байтам
aes-ctr16, 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:

  1. Таблица настроек преобразуется в JSON UTF-8.
  2. Данные дополняются PKCS#7 до границы 8 байтов.
  3. Создаётся новый 8-байтный IV и выполняется шифрование RTEA CBC.
  4. 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 не меняется. В диагностических выгрузках пароли по-прежнему скрываются.