Не удается преобразовать строку в UTF-16LE для расчета MD5 на Arduino
TLDR
Мне нужно преобразовать текст с веб-сайта в формат UTF-16LE, чтобы получить правильную контрольную сумму MD5, но я не могу понять, как это сделать. Все это происходит на Arduino для входа в маршрутизатор.
Фон
Я хочу считать значения с устройства «умный дом», подключенного к маршрутизатору Fritz!Box с помощью Arduino с подключением к сети Ethernet. Маршрутизатор имеет открытый API, и я использую HTTP (EN|DE), чтобы получить значения. Программирование на C++ не моя сильная сторона.
Я основывал свои попытки на BASH. и примеры javascript. Для расчета MD5 я использую библиотеку ArduinoMD5 от tzikis.
Шаги
В соответствии с инструкцией вам необходимо сделать следующее:
- Свяжитесь с маршрутизатором, чтобы получить строку запроса (маршрутизатор отправляет XML)
- Создайте ответ, вычислив контрольную сумму MD5, используя строку запроса и пароль:
<challenge>-<password>
- Отправить ответ,
<challenge>-<response>
, через POST - Получить ответ XLM от маршрутизатора, содержащий SID.
- Используйте SID для запроса данных или управления устройством умного дома.
Я застрял на шаге 2. У меня есть задача, но я не могу вычислить правильную контрольную сумму.
В руководстве говорится:
Хэш MD5 создается из последовательности байтов UTF-16LE. кодирование этой строки (без BOM и без завершающих 0 байтов).
Попытки
Пока что я могу запросить вызов: например, 70067288. Ответ, кажется, всегда буквенно-цифровой. Мой пароль тоже буквенно-цифровой.
unsigned char* hash2 = MD5::make_hash("1234567z-äbc");
char *md5str2 = MD5::make_digest(hash2, 16);
free(hash2);
Serial.print("MD5: ");
Serial.println(md5str2);
Это была моя попытка подтверждения. Руководство дало 1234567z
в качестве примера вызова с äbc
(в примере действительно есть умлауты) в качестве пароля. Ответ, показанный для этого примера, был 1234567z-9e224a41eeefa284df7bb0f26c2913e2
, но моя контрольная сумма из приведенного выше кода была 935fe44e659beb5a3bb7a4564fba0513
.
Я попытался "вручную" создание UTF-16LE и вычисление MD5, но это тоже не сработало.
byte rawTest[] = {0x3100, 0x3200, 0x3300, 0x3400, 0x3500, 0x3600, 0x3700, 0x7a00, 0x2d00, 0xe400, 0x6200, 0x6300};
char buffer[12] = {};
unsigned char* hash2 = MD5::make_hash(&buffer[0]);
char *md5str2 = MD5::make_digest(hash2, 16);
free(hash2);
Serial.print("MD5: ");
Serial.println(md5str2);
Это дало мне d41d8cd98f00b204e9800998ecf8427e
.
Я не совсем уверен, что приведенный пример был правильно рассчитан. Используя их пример в документации API с кодом BASH с упомянутого выше сайта, я получаю другой ответ: 1234567z-bb6e5b7c7d4d485590f4e084ad3da989
.
#!/bin/bash
# -----------
# definitions
# -----------
FBF="http://192.168.178.1/"
USER="root"
PASS="äbc"
AIN="AINOFYOURFRITZDECTDEVICE"
# ---------------
# fetch challenge
# ---------------
CHALLENGE="1234567z"
# -----
# login
# -----
MD5=$(echo -n ${CHALLENGE}"-"${PASS} | iconv -f ISO8859-1 -t UTF-16LE | md5sum -b | awk '{print substr($0,1,32)}')
RESPONSE="${CHALLENGE}-${MD5}"
echo $RESPONSE
Мы будем очень признательны за любую помощь в его работе.
Рабочий пример (с оговорками)
Требования:
- Пароль, использующий только символы из ASCII.
- Пароль должен содержать не менее 17 символов.
В пароле должны использоваться только символы ASCII, поскольку он легко преобразуется в UTF-16LE.
Я не могу объяснить, почему пароль должен быть не менее 17 символов. Мой код проверяет длину фактического пароля программно.
С паролем длиной не менее 17 символов я получаю что-то вроде этого:
Вызов: 6c607ee5
Пропуск испытания (26): 6c607ee5-01234567890123456
Длина предварительного распределения Challenge Pass: 26
MD5 (UTF-16LE): 3b26241ae4aab8eaf71d5c1599932178
Любой короткий пароль, и я получаю что-то вроде этого:
Вызов: 621611e1
Пропуск испытания (26): 621611e1-0123456789012345
Длина предварительного распределения Challenge Pass: 25
MD5 (UTF-16LE): 30a163ab8a267adfbb524b2b7412e81b
Прошу прощения за плохое кодирование, C++ не моя сильная сторона.
void fritzboxSID() {
EthernetClient fritzBox;
const char* pass = "01234567890123456"; // минимум 17 символов ASCII
const char* user = "fritz3456"; // Учетная запись на роутере, случайно сгенерированная или созданная пользователем
const size_t passLength = strlen(pass); // Длина пароля
char c;
uint8_t xml = 0;
char challenge[9] = {0}; // Вызов
char challengePass[8 + 1 + passLength]; // вызов-пароль
uint8_t challengeCount = 0;
if (fritzBox.connect("192.168.200.1", 80)) {
// Отправляем запрос на вход
fritzBox.println("GET /login_sid.lua HTTP/1.1");
fritzBox.println("Host: 192.168.200.1");
fritzBox.println("Connection: close");
fritzBox.println();
while (fritzBox.connected()) {
while (fritzBox.available()) {
c = fritzBox.read(); // Чтение одного символа из ответа маршрутизатора
if (c == '>') {
xml++;
}
if (xml == 5) {
// Вызов начинается после пятого >
if (challengeCount > 0 & challengeCount < 9) {
// Сохраняем вызов, символ за символом
challenge[challengeCount-1] = c;
challengePass[challengeCount-1] = c;
}
challengeCount++;
}
Serial.print(c); // Печатаем HTTP-ответ
}
}
challengePass[8] = '-';
for (size_t i = 0;i < passLength; i++){
// Копируем пароль в комбинированную строку пароль-запрос
challengePass[i+9] = pass[i];
}
// Показать вызов и пройти, проверяя длины
// Если strlen(challengePass) не равен длине предварительного выделения, то MD5 будет неправильным
Serial.println("");
Serial.print("Challenge: ");
Serial.println(challenge);
Serial.print("Challenge Pass (");
Serial.print(strlen(challengePass));
Serial.print("): ");
Serial.println(challengePass);
Serial.print("Challenge Pass Preallocation Length: ");
Serial.println(8 + 1 + passLength);
// Преобразование в UTF-16LE (работает только для стандартных символов ASCII)
const size_t length = strlen(challengePass);
char buffer[2*length];
for (size_t i = 0; i < length; i++) {
buffer[2*i] = challengePass[i];
buffer[2*i+1] = 0;
}
// Сгенерировать MD5
unsigned char* hash = MD5::make_hash(buffer, 2*length);
char *md5str = MD5::make_digest(hash, 16);
free(hash);
Serial.print("MD5 (UTF-16LE): ");
Serial.println(md5str);
} else {
Serial.println("Cannot connect to 192.168.200.1");
}
}
Если это поможет, ответ маршрутизатора на первоначальный запрос на вход будет таким:
HTTP/1.1 200 OK
Cache-Control: no-cache
Connection: close
Content-Type: text/xml
Date: Fri, 16 Sep 2022 08:21:27 GMT
Expires: -1
Pragma: no-cache
X-Frame-Options: SAMEORIGIN
X-XSS-Protection: 1; mode=block
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'; connect-src 'self'; font-src 'self'; frame-src https://service.avm.de https://help.avm.de https://www.avm.de https://avm.de https://assets.avm.de https://clickonce.avm.de http://clickonce.avm.de http://download.avm.de https://download.avm.de 'self'; img-src 'self' https://tv.avm.de https://help.avm.de/images/ http://help.avm.de/images/ data:; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; frame-ancestors 'self'; media-src 'self'
<?xml version="1.0" encoding="utf-8"?><SessionInfo><SID>0000000000000000</SID><Challenge>621611e1</Challenge><BlockTime>0</BlockTime><Rights></Rights><Users><User last="1">fritz3456</User><User>UserName</User></Users></SessionInfo>
@Jeremy, 👍2
Обсуждение1 ответ
Лучший ответ:
Вы должны преобразовать строку "<задание>-<пароль>" к УТФ-16ЛЕ. Если вся строка представляет собой простой ASCII, преобразование тривиально: вам просто нужно добавить нулевой байт после каждого байта ASCII.
// Строка ASCII для хеширования.
const char* text = "1234567z-abc"; // умлаут удален
// Преобразование в UTF-16LE.
const size_t length = strlen(text);
char buffer[2*length];
for (size_t i = 0; i < length; i++) {
buffer[2*i] = text[i];
buffer[2*i+1] = 0;
}
// Вычислить и распечатать хэш.
unsigned char* hash2 = MD5::make_hash(buffer, 2*length);
char *md5str2 = MD5::make_digest(hash2, 16);
free(hash2);
Serial.print("MD5: ");
Serial.println(md5str2);
Обратите внимание, что я использую двухпараметрическую версию MD5::make_hash()
, чтобы
для передачи длины буфера. Версия с одним параметром
метод остановится на первом нулевом байте.
Правка: относительно вашего расширенного вопроса ( новый раздел «Рабочие Пример»), это проблема, совершенно не связанная с предыдущей. Речь идет о завершении строки.
В C и C++ строки представляют собой массивы символов, оканчивающиеся символом NUL. Символ ASCII (числовое значение ноль). Завершающий NUL не является частью строка как таковая, но она должна храниться в массиве, содержащем нить. Строковые литералы интерпретируются компилятором как Массивы символов, заканчивающиеся NUL. Каждая функция, которая ожидает строка в качестве входных данных ожидает, что она будет завершена NUL.
В этом конкретном случае вам не удалось выполнить NUL-завершение
ChallengePass
. Когда этот массив передается strelen()
, эта функция
сканирует память, пока не найдет нулевой байт. Если этот нулевой байт произойдет
быть сразу после конца массива, ваш код будет работать так, как ожидалось.
В противном случае любой ненулевой байт, идущий после challengePass
в памяти, будет
интерпретироваться как часть строки. Поведение программы такое
непредсказуемый, потому что вы не знаете, что будет храниться в памяти
сразу после challengePass
.
Решение состоит в том, чтобы завершить строку NUL. Начните с выделения одного больше байт:
char challengePass[8 + 1 + passLength + 1]; // вызов-пароль
Затем сразу после копирования пароля добавьте нулевой байт для завершения строка:
challengePass[8 + 1 + passLength] = '\0'; // завершаем строку
Обратите внимание, что '\0'
означает "символ с нулевым числовым значением".
В качестве альтернативы вы можете завершить строку сразу после добавления дефиса,
затем используйте strcat()
для объединения пароля. strcat()
займет
позаботьтесь о завершении строки:
challengePass[8] = '-';
challengePass[9] = '\0'; // временно завершаем строку
strcat(challengePass, pass); // добавляем пароль
Это на самом деле работает, но по какой-то причине я генерирую текст
, это не удается. Если я затем вручную введу новый вызов и тот же пароль в const char* text...
, он даст правильный MD5.
Операции с символами/строками С++ сильно запутывают., @Jeremy
@Jeremy: Что вы подразумеваете под «_Я генерирую текст
, он не работает_»? Каким образом это терпит неудачу? Мне не важно, набран ли текст в исходнике или сгенерирован программно., @Edgar Bonet
Я попытался опубликовать код в комментарии, но это был полный беспорядок.
`
cpp
const char* pass = "myPassword";
const size_t passLength = strlen(pass);
char вызов[9] = {0}; // Вызов
char вызовPass[8 + 1 + passLength]; // вызов-пароль
`
Используя модуль ethernet, я затем прочитал XML из маршрутизатора, скопировав его посимвольно в challenge
и challengePass
. После этого я попытался с помощью strcat
построить строку вызова-пароля для подачи в функцию MD5. Однако MD5 всегда ошибался. Это потому, что strcat
добавляет в конец нуль-терминатор?, @Jeremy
@Jeremy: Вопрос о конкатенации строк в вашем последнем комментарии: 1. Не связан с текущим вопросом, касающимся преобразования ASCII → UTF16-LE. 2. Невозможно ответить, не видя, как вы на самом деле пытаетесь объединить строки., @Edgar Bonet
Весь этот вопрос находится в контексте сборки строки, чтобы не просто генерировать _любую_ контрольную сумму MD5, но и _правильную_ контрольную сумму MD5. Подробности очень важны. Я понимаю, что это Stack Exchange, и второстепенные вопросы следует задавать отдельно. Теперь, чтобы увидеть, могу ли я выяснить другие необходимые шаги. Спасибо еще раз за помощь. Если я или кто-то другой выясню предостережение о длине пароля, я обновлю решение., @Jeremy
@Jeremy: см. расширенный ответ., @Edgar Bonet
Я не знаю, стоит ли что-то менять в ответе для этого, но может быть полезно знать, что у вас, по-видимому, есть возможность использовать набор функций более низкого уровня (MD5_CTX
, MD5Init
, MD5Update
, MD5Final), которые позволяют избежать временного буфера, динамического выделения памяти и конкатенации. Различные конкатенации, в том числе 0x00, выполняемые для преобразования ASCII в UTF-16LE, становятся отдельными вызовами MD5Update., @timemage
Спасибо за совет. Я медленно преобразовываю все это в полезные функции. Как только доберусь до оптимизации, попробую. Без сомнения, существует множество способов оптимизировать код _my_ C++., @Jeremy
- C++ против языка Arduino?
- Как использовать SPI на Arduino?
- Какие накладные расходы и другие соображения существуют при использовании структуры по сравнению с классом?
- Ошибка: expected unqualified-id before 'if'
- Что лучше использовать: #define или const int для констант?
- Функции со строковыми параметрами
- Библиотека DHT.h не импортируется
- ошибка: ожидаемое первичное выражение перед токеном ','
1. Параметр iconv
-f ISO8859-1
выглядит подозрительно. Вы уверены, что используете этот старый устаревший набор символов? Ваш сценарий bash дает мне ожидаемый результат, если я удалю эту опцию. 2. Можете ли вы использовать пароль, состоящий только из ASCII? Если это так, перевод в UTF16LE будет практически тривиальным., @Edgar BonetХорошая находка. Мой пароль только ASCII. Приведенного в документации примера не было. Как бы вы преобразовали его в UTF-16LE в Arduino C++ в этом случае? Мне все еще нужно программно преобразовать вызов. Спасибо за быстрый ответ., @Jeremy
byte rawTest[] = {0x3100, 0x3200,.....
это не то, что вы думаете. Я предполагаю, что вы не искали творческий способ заполнить свой массив значениями 0x00. Вероятно, вы имели в виду0x31, 0x00, 0x32, 0x00, .....
, @timemageХорошо знать. Если я когда-нибудь доберусь до расширения кода из паролей ASCII, я попробую это. Давайте посмотрим, насколько сложно сначала завершить процесс входа в систему и получить данные., @Jeremy