При интеграции систем 1С с внешними сервисами на PHP часто возникает необходимость формировать подпись запросов по алгоритму HMAC-SHA512. Однако разработчики регулярно сталкиваются с ситуацией, когда одинаковый исходный текст и секретный ключ выдают абсолютно разные хэши в 1С и в PHP.
В этой статье мы подробно разберем основные причины расхождений, проанализируем ошибки работы с кодировками и двоичными данными в 1С, а также рассмотрим правильный способ расчета хэша HMAC-SHA512.
Разберем сначала простую, но распространенную синтаксическую ошибку на стороне PHP. Если передавать подписываемую строку в двойных кавычках, например "te$st", интерпретатор PHP воспринимает конструкцию $st как переменную. В результате на выходе получается строка "te" вместо "te$st".
Для корректного сравнения с 1С на стороне PHP необходимо экранировать знак доллара или использовать одинарные кавычки:
Неправильно в PHP:
$sign = hash_hmac('sha512', "te$st", "123");
Правильно в PHP:
$sign = hash_hmac('sha512', 'te$st', '123');
Или с экранированием:
$sign = hash_hmac('sha512', "te\$st", "123");
Более глубокая техническая проблема возникает при работе с секретным ключом, закодированным в Base64. Очень часто секретные ключи подписи передаются в формате Base64, например: sO017E2K/s0VLtKNxd5HwOYbRdt/sA1Bv0QECa0+Wv7CHKDyKG0o7puTvP8/OQA/Nr3dCvPUpztzOKc6/wXj+g==.
В PHP функция base64_decode() превращает такую строку в «сырой» массив байт (сырую бинарную строку):
$secret = base64_decode($key);
В 1С разработчики часто совершают ошибку, сначала декодируя Base64 через Base64Значение(), а затем преобразуя получившиеся ДвоичныеДанные в обычную строку с помощью ПолучитьСтрокуИзДвоичныхДанных() или чтения текстовым документом.
Почему так делать нельзя: произвольный массив байт (значения от 0x00 до 0xFF) не является валидным текстом в кодировке UTF-8 или ANSI. При попытке интерпретировать сырые байты как текст происходят необратимые искажения: служебные символы перевода строк (0x0D, 0x0A) и некорректные байтовые последовательности заменяются или теряются. Когда эта искаженная строка снова переводится в байты через System.Text.UTF8Encoding, получаются совершенно другие байты ключа, чем в PHP.
Чтобы хэш совпал с PHP, секретный ключ после декодирования Base64 должен передаваться в COM-объект System.Security.Cryptography.HMACSHA512 непосредственно как массив байт (COMSafeArray с типом VT_UI1), без какого-либо перевода в строковый тип 1С.
Рассмотрим пошаговое решение этой задачи.
ДвоичныеДанные.COMSafeArray с типом элементов VT_UI1.Key COM-объекта HMACSHA512.Ниже приведен готовый пример кода для 1С:
Функция ХешФункцияSHA512_Base64Ключ(Знач Строка, Знач СекретныйКлючBase64) Экспорт
// 1. Создаем COM-объекты .NET
Encoding = Новый COMОбъект("System.Text.UTF8Encoding");
HMAC = Новый COMОбъект("System.Security.Cryptography.HMACSHA512");
// 2. Получаем двоичные данные ключа из Base64
ДвоичныеДанныеКлюча = Base64Значение(СекретныйКлючBase64);
// 3. Преобразуем ДвоичныеДанные в COMSafeArray (массив байтов)
МассивБайтКлюча = ДвоичныеДанныеВБайтыCOM(ДвоичныеДанныеКлюча);
HMAC.Key = МассивБайтКлюча;
// 4. Вычисляем хэш от исходной строки
БайтыСтроки = Encoding.GetBytes_4(Строка);
ХешБайт = HMAC.ComputeHash_2(БайтыСтроки).Выгрузить();
// 5. Переводим результат в шестнадцатеричную строку (hex)
Хеш = "";
Для Каждого Байт Из ХешБайт Цикл
Символ = DecToHex(Байт);
Хеш = Хеш + ДобавитьЛидирующиеНули(Символ, 2);
КонецЦикла;
Возврат Хеш;
КонецФункции
Функция ДвоичныеДанныеВБайтыCOM(ДвоичныеДанные)
// Переводим двоичные данные в Hex-строку и разбиваем по байтам
ХексСтрока = ЗначениеВHexСтроку(ДвоичныеДанные);
МассивБайт = Новый Массив;
Длина = СтрДлина(ХексСтрока);
Для Индекс = 1 По Длина / 2 Цикл
БайтHex = Сред(ХексСтрока, (Индекс - 1) * 2 + 1, 2);
МассивБайт.Добавить(HexToDec(БайтHex));
КонецЦикла;
// Возвращаем COMSafeArray с типом элементов VT_UI1 (unsigned 8-bit integer)
Возврат Новый COMSafeArray(МассивБайт, "VT_UI1", МассивБайт.Количество());
КонецФункции
Функция HexToDec(Знач HexСтрока)
СоответствиеHEX = Новый Соответствие;
СоответствиеHEX.Вставить("0", 0); СоответствиеHEX.Вставить("1", 1);
СоответствиеHEX.Вставить("2", 2); СоответствиеHEX.Вставить("3", 3);
СоответствиеHEX.Вставить("4", 4); СоответствиеHEX.Вставить("5", 5);
СоответствиеHEX.Вставить("6", 6); СоответствиеHEX.Вставить("7", 7);
СоответствиеHEX.Вставить("8", 8); СоответствиеHEX.Вставить("9", 9);
СоответствиеHEX.Вставить("A", 10); СоответствиеHEX.Вставить("B", 11);
СоответствиеHEX.Вставить("C", 12); СоответствиеHEX.Вставить("D", 13);
СоответствиеHEX.Вставить("E", 14); СоответствиеHEX.Вставить("F", 15);
СоответствиеHEX.Вставить("a", 10); СоответствиеHEX.Вставить("b", 11);
СоответствиеHEX.Вставить("c", 12); СоответствиеHEX.Вставить("d", 13);
СоответствиеHEX.Вставить("e", 14); СоответствиеHEX.Вставить("f", 15);
Старший = СоответствиеHEX[Сред(HexСтрока, 1, 1)];
Младший = СоответствиеHEX[Сред(HexСтрока, 2, 1)];
Возврат Старший * 16 + Младший;
КонецФункции
Функция DecToHex(Знач Число)
Результат = "";
Пока Число <> 0 Цикл
Поз = Число % 16;
Результат = Сред("0123456789abcdef", Поз + 1, 1) + Результат;
Число = Цел(Число / 16);
КонецЦикла;
Возврат Результат;
КонецФункции
Функция ДобавитьЛидирующиеНули(Знач Строка, ЧислоСимволов)
ВозвращаемаяСтрока = Строка(Строка);
Длина = СтрДлина(ВозвращаемаяСтрока);
Для й = 1 По ЧислоСимволов - Длина Цикл
ВозвращаемаяСтрока = "0" + ВозвращаемаяСтрока;
КонецЦикла;
Возврат ВозвращаемаяСтрока;
КонецФункции
Проанализируем важный нюанс: использование COM-объектов System.Security.Cryptography работает исключительно на серверах под управлением ОС Windows. Если ваш сервер 1С запущен на Linux или вы разработали мобильное приложение, данный подход вызовет ошибку отсутствия COM-компонента.
Для кроссплатформенной работы на Linux-серверах можно использовать следующие альтернативы:
OpenSSL через метод ЗапуститьПриложение().ХешированиеДанных (функция ХешФункция.SHA512) путем создания двух проходов хеширования с ключом (ipad и opad).