Не удается преобразовать строку в UTF-16LE для расчета MD5 на Arduino

c++

TLDR

Мне нужно преобразовать текст с веб-сайта в формат UTF-16LE, чтобы получить правильную контрольную сумму MD5, но я не могу понять, как это сделать. Все это происходит на Arduino для входа в маршрутизатор.

Фон

Я хочу считать значения с устройства «умный дом», подключенного к маршрутизатору Fritz!Box с помощью Arduino с подключением к сети Ethernet. Маршрутизатор имеет открытый API, и я использую HTTP (EN|DE), чтобы получить значения. Программирование на C++ не моя сильная сторона.

Я основывал свои попытки на BASH. и примеры javascript. Для расчета MD5 я использую библиотеку ArduinoMD5 от tzikis.

Шаги

В соответствии с инструкцией вам необходимо сделать следующее:

  1. Свяжитесь с маршрутизатором, чтобы получить строку запроса (маршрутизатор отправляет XML)
  2. Создайте ответ, вычислив контрольную сумму MD5, используя строку запроса и пароль: <challenge>-<password>
  3. Отправить ответ, <challenge>-<response>, через POST
  4. Получить ответ XLM от маршрутизатора, содержащий SID.
  5. Используйте 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>

, 👍2

Обсуждение

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


1 ответ


Лучший ответ:

2

Вы должны преобразовать строку "<задание>-<пароль>" к УТФ-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