Что такое HMAC и JWT и как это использовать в 1С


Лёгкая статья про стандарты HMAC и JWT с небольшой теорией и исходным кодом.

Лёгкая статья про стандарты HMAC и JWT с небольшой теорией и исходным кодом.

Коллеги, позвольте вам рассказать о вещах, которые далеки от обычной разработки в 1С.

Вероятно, что сейчас у вас нет необходимости использовать HMAC и JWT, но время не стоит на месте, и, возможно, в очередном проекте интеграции вы задействуете описываемые ниже стандарты.

Часть первая, теоретическая.

HMAC (Hash-based message authentication code) — это хэш, который вычисляется, основываясь на двух значениях: ‘ключ’ и ‘сообщение’. Такой хэш нужен, чтобы гарантировать, что данные, передаваемые в ненадежной среде, не были изменены посторонними лицами.

Зачем же нам использовать HMAC, когда у нас есть, например, HTTPS?

HMAC полезен, когда участников в обмене сообщениями больше, чем два.

Например, у нас есть три участника:

  •  ‘Провайдер API’ — сторонний сервис, который принимает запросы на отправку открыток и букетов
  •  ‘Сервер’ — серверная часть вашего приложения
  •  ‘Клиент’ — клиентская часть, с которой работают пользователи

‘Провайдер API’ предоставляет две вещи для выполнения запросов – это AccounID и SecretKey. ‘Провайдер API’ ни чего не знает про ваших пользователей, чтобы принять запрос на отправку открытки, ему нужно удостовериться, что просящий знает AccounID и SecretKey.

‘Сервер’ в свою очередь должен управлять тем, какие пользователи имеют право отправлять букеты, а каким разрешены только открытки.

Разумеется, очевидное решение задачи заключается в том, чтобы все клиентские запросы направить через ‘Сервер’:

  1.  ‘Клиент’ — формирует запрос на отправку открытки для ‘Сервера’;
  2.  ‘Сервер’ — проверяет права конкретного пользователя и если всё хорошо, то перенаправляет вызов на ‘Провайдера API’;
  3.  ‘Провайдер API’ делает необходимые действия;
  4. Далее по цепочке обратно передается результат вызова.

Но что, если у нас клиенты генерируют десятки тысяч таких запросов, и не хотят долго ждать результат выполнения? Или наши запросы содержат потоковое аудио (для музыкальных открыток), которым не хотелось бы грузить ‘Сервер’? Более того, а что, если ‘Клиент’ передает конфиденциальные сведения, которые и вовсе не должны попасть на ‘Сервер’ (букет с интимным посланием)?
Почему бы нам сразу не слать запросы с ‘Клиента’ на ‘Провайдер API’?

Тогда нам придется ‘Клиенту’ сообщить AccounID и SecretKey, которые нужны ‘Провайдеру API’. Но поскольку у нас разные клиенты имеют разные права (открытки, букеты) и в какой то момент у клиента права могут быть и вовсе отозваны, то мы не можем сообщать ‘Клиенту’ AccounID и SecretKey.
В этот момент нам и пригодится HMAC.
Благодаря HMAC мы можем построить работу следующим образом:

  1. ‘Клиент’ запрашивает у ‘Сервера’ специальный Token
  2. ‘Сервер’ делает Token с помощью HMAC, учитывая права пользователя и ограничивая действие токена по времени:
Token = HMAC(SecretKey, AccounID + ПравоПользователя + ДатаВремяДоступа)

Теперь пользователь может обратиться напрямую к ‘Провайдеру API’ за конкретной услугой и предоставить Token, AccounID и ДатаВремяДоступа. ‘Провайдер API’ вычисляет Token и сверяет его с тем, что прислал ‘Клиент’ и той услугой которую ‘Клиент’ хочет получить.

В данной схеме ‘Клиент’ является той самой ненадежной средой. Фактически запрос должен делать ‘Сервер’, потому что ‘Провайдер API’ сказал свой SecretKey только ‘Серверу’. Но наш ‘Сервер’ не хочет делать запросы и разрешает на время ‘Клиенту’ самостоятельно делать запросы. Наш ‘Сервер’ не доверяет ‘Клиенту’ и использует HMAC, чтобы обеспечить неизменность выданных разрешений на использование услуг ‘Провайдера API’.

Часть вторая, считаем HMAC.

Давайте посмотрим под капот и узнаем, что же внутри функции HMAC.

Функция HMAC на вход принимает SecretKey и Message типа ДвоичныеДанные и вид хэш функции.

Function HMAC(Val SecretKey, Val Message, Val HashFunc) Export

BlSz = 64;

// Если ключ больще чем размер блока, то в качестве ключа используем хэш от ключа
If SecretKey.Size() > BlSz Then
SecretKey = Hash(SecretKey, HashFunc);
EndIf;

EmptyBin = GetBinaryDataFromString("");
SecretKey = BinLeft(SecretKey, BlSz);

// Если ключ меньше блока, то добиваем его нулями до размера блока
К0 = BinRightPad(SecretKey, BlSz, "0x00");

// Делаем k_ipad, выполняем операцию «побитовое исключающее ИЛИ» ключа c константой 0x36
ipad = BinRightPad(EmptyBin, BlSz, "0x36");
k_ipad = BinBitwiseXOR(К0, ipad);

// Делаем k_opad, выполняем операцию «побитовое исключающее ИЛИ» ключа c константой 0x5c
opad = BinRightPad(EmptyBin, BlSz, "0x5C");
k_opad = BinBitwiseXOR(К0, opad);

// склеиваем k_ipad и сообщение
k_ipad_Message = BinConcat(k_ipad, Message);

// вычисляем хэши и получаем результат
k_opad_Hash = BinConcat(k_opad, Hash(k_ipad_Message, HashFunc));
res = Hash(k_opad_Hash, HashFunc);

Return res;

EndFunction

Функции BinLeft, BinRightPad, BinBitwiseXOR и BinConcat реализованы с использованием возможностей платформы, которые появились в версии 8.3.10.2168.

Теперь мы можем делать хэш и комбинировать данные для подписи любым способом.

Например, так:

Token = HMAC(SecretKey, AccounID + ПравоПользователя + УникальныйИдентификатор + "допДанные7" + ДатаВремяДоступа + ИмяПользователя)

Такая свобода действий может породить хаос и как следствие – увеличение сроков разработки и интеграции. Что, если бы у нас был стандарт, по которому определен формат для сообщений?

Такой стандарт у нас есть, и называется он JWT.

Часть третья, пару слов про JWT

JWT (JSON Web Token) – это стандарт, по которому определено, в каком виде будет выглядеть токен для клиента. По сути JWT это строка, состоящая из трех частей соединенных точками: заголовок, данные и подпись.

Например: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Этот токен содержит:

заголовок (Header)

{
"alg": "HS256",
"typ": "JWT"
}

данные (Payload)

{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

и подпись (Signature):

TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Польза от JWT в том, что это стандарт и для этого стандарта уже готовы библиотеки на всевозможных платформах.

К этой статье приложена реализация JWT на чистом 1C:Enterprise 8.3.10.2168.

Например, вы делаете интеграцию со сторонним сервисом, и вам необходимо договорится об авторизации запросов от пользователей. С большой вероятностью сторонний сервис реализован на платформе, для которой есть готовая библиотека JWT. Вы как прогрессивный разработчик 1С теперь можете предложить использовать стандарт RFC 7519 – т.е. JWT. Каждый со своей стороны возьмет готовую библиотеку и вуаля – пользователи ходят напрямую в сторонний сервис.

 

Заключение

Благодарю, что прочитали эту статью. Надеюсь, вы нашли для себя полезное.

Пожалуйста, напишите в комментариях, как по вашему мнению можно было бы использовать JWT в мире 1С уже сейчас.

Любые, возможно бредовые, идеи принимаются. Например: отправка whatsapp-сообщений прямо из тонкого клиента.

40 Comments

  1. stas1976

    спасибо

    Reply
  2. NcSteel

    Подскажите, пожалуйста, вариацию если в качестве соли используется не строковый ключ, а сертификат

    Reply
  3. user757186

    (3) функции хэширования и подписи работают с двоичными данными, строки — это лишь частный случай двоичных данных. Если вы посмотрите внимательно код — там нет фукнкций работы со строками, там используются функции работы с двоичными данными.

    Reply
  4. pbazeliuk

    Для MD5 и SHA-1 у вас не верный размер блока, он должен быть 64. Можете проверить в любом онлайн генераторе.

    Reply
  5. pbazeliuk

    И последнее, если ключ больше размера блока, его необходимо предварительно хешировать

    Reply
  6. keypax

    (5)

    да, вы правы, спасибо.

    Reply
  7. keypax

    (6) Обновил код по вашим замечаниям. Благодарю, что нашли ошибки.

    Reply
  8. moro_as

    задача в интеграции с https://docs.veeroute.com/api/examples/json_examples/

    есть что-то общее между JWT?

    застрял на функции getBytes() какой аналог в 1с (двоичные данные?)

    направьте куда копать.

    Reply
  9. keypax

    (9) да

    в строчке

    byte[] passBytes = password.getBytes();

    по всей видимости следует использовать глобальный метод ПолучитьДвоичныеДанныеИзСтроки (GetBinaryDataFromString)

    Затем вам нужно будет конвертировать массив в двоичные данные

    byte[] saltBytes = listToArray(ticket.getSalt());

    Для этого вам понадобится объект ЗаписьДанных (DataWriter)

    и метод ЗаписатьБайт (WriteByte)

    Далее нужно будет соеденить passBytes, saltBytes и вычислить хэш

    byte[] PaS = makeBytesHash(concatenateByteArrays(passBytes, saltBytes), «SHA-512»);

    Для соединения passBytes, saltBytes используйте функцию из статьи

    Function BinConcat(Val BinaryData1, Val BinaryData2)

    А вот с хэшем беда.

    В 1С есть только хэш-функции

    CRC32

    MD5

    SHA1

    SHA256

    Поэтому ищем в интернете варианты. Например в этой статье есть два подхода для получения SHA-512:

    https://infostart.ru/public/175332/

    Осталось только полученный хэш конвертировать в массив — для этого используйте объект ЧтениеДанных (DataReader).

    Reply
  10. moro_as

    (10) Спасибо огромное!!!

    Затем вам нужно будет конвертировать массив в двоичные данные

    byte[] saltBytes = listToArray(ticket.getSalt());

    Для этого вам понадобится объект ЗаписьДанных (DataWriter)

    и метод ЗаписатьБайт (WriteByte)

    Функция listToArray(Знач Массив) Экспорт
    Перем Результат;
    ПотокВПамяти = Новый ПотокВПамяти();
    ЗаписьДанных = Новый ЗаписьДанных(ПотокВПамяти);
    Для Каждого Элемент из Массив Цикл
    ЗаписьДанных.ЗаписатьБайт(Число(Элемент));
    КонецЦикла;
    Результат = ПотокВПамяти.ЗакрытьИПолучитьДвоичныеДанные();
    Возврат Результат;
    КонецФункции

    Показать

    ЗаписатьБайт выдаёт ошибку:

    по причине:

    Значение байта должно быть в диапазоне от 0 до 255,

    а в передаваемом массиве [-100,-96,20,51,89,-47,-80,-128,86,43,-24,124,-109,12,47] есть элементы с минусовым значением…

    Reply
  11. moro_as

    (11) Оказывается байт java (-127…128), вот и вся ошибка.

    Reply
  12. moro_as

    (10)

    Для соединения passBytes, saltBytes используйте функцию из статьи

    Function BinConcat(Val BinaryData1, Val BinaryData2)

    штатным образом можно получить через СоединитьДвоичныеДанные

    Reply
  13. moro_as

    (10)

    А вот с хэшем беда.

    В 1С есть только хэш-функции

    CRC32

    MD5

    SHA1

    SHA256

    Поэтому ищем в интернете варианты. Например в этой статье есть два подхода для получения SHA-512:

    https://infostart.ru/public/175332/

    Да, действительно беда.

    Но можно и вот так попробовать:

    Функция CryptoSHA512(Строка) Экспорт
    Текст = Новый COMОбъект(«System.Text.UTF8Encoding»);
    КриптоSHA512 = Новый COMОбъект(«System.Security.Cryptography.SHA512Managed»);
    Байты = Текст.GetBytes_4(Строка);
    МассивХэш = КриптоSHA512.ComputeHash_2(Байты).Выгрузить();
    Возврат МассивХэш;
    КонецФункции
    

    Показать

    Reply
  14. Varsaavius

    При использовании алгоритма RS256, при декодировании постоянно не совпадают сигнатуры. /:

    Reply
  15. pallid

    Подскажите есть ли замена платформенного метода Buffer1.WriteBitwiseXor(0, Buffer2, Buffer2.Size) на аналог

    Function BinBitwiseXOR(Val BinaryData1, Val BinaryData2)
    
    MemoryStream = New MemoryStream();
    DataWriter = New DataWriter(MemoryStream);
    
    DataReader1 = New DataReader(BinaryData1);
    DataReader2 = New DataReader(BinaryData2);
    
    Buffer1 = DataReader1.ReadIntoBinaryDataBuffer();
    Buffer2 = DataReader2.ReadIntoBinaryDataBuffer();
    
    If Buffer1.Size > Buffer2.Size Then
    Buffer1.WriteBitwiseXor(0, Buffer2, Buffer2.Size);
    DataWriter.WriteBinaryDataBuffer(Buffer1);
    Else
    Buffer2.WriteBitwiseXor(0, Buffer1, Buffer1.Size);
    DataWriter.WriteBinaryDataBuffer(Buffer2);
    EndIf;
    
    res = MemoryStream.CloseAndGetBinaryData();
    Return res;
    
    EndFunction
    
    

    Показать

    Reply
  16. keypax

    (16) К сожалению, у меня нет готового кода, которы бы заменял метод WriteBitwiseXor

    Reply
  17. frkbvfnjh

    Выдает ошибку:

    Процедура или функция с указанным именем не определена (ValueIsNotFilled)

    Reply
  18. frkbvfnjh

    Блин капец:

    Обработка.HMAC_JWT.Форма.Form.Форма Возможно ошибочный метод: «DecodeHS256»

    Обработка.HMAC_JWT.Форма.Form.Форма Возможно ошибочный метод: «DecodeRS256»

    Обработка.HMAC_JWT.Форма.Form.Форма Возможно ошибочный метод: «EncodeHS256»

    Обработка.HMAC_JWT.Форма.Form.Форма Возможно ошибочный метод: «EncodeRS256»

    Нет таких функций!

    Вы бы перепроверили все. И раз уж на то пошло, то выкладывали бы сразу все в виде одного файла демонстрационной конфигурации или все функции в обработку засунули, а не кучу разрозненных файлов

    Reply
  19. frkbvfnjh

    Я так понимаю есть только Encode и Decode и кодирование производится только по алгоритму SHA256. Все верно?

    Reply
  20. keypax

    (18)

    Пожалуйста добавьте метод

    Function ValueIsNotFilled(Value) Export
    
    Return Not ValueIsFilled(Value);
    
    EndFunction
    
    Reply
  21. keypax

    (19) код скаченных модулей Cryptography и JWT рабочий.

    Демонстрационная обработка использует интерфейс от несколько обновленного модуля JWT.

    В связи с этим обновил публикацию — залил последнюю версию модуля JWT.

    Прошу прощения за неудобства.

    Вышлю вам последнюю версию в ЛС.

    Reply
  22. keypax

    (20) средствами 1С реализован только алгоритм HS256. Для алгоритма RS256 я использовал COM-объект из сторонней библиотеки.

    Reply
  23. frkbvfnjh

    (23) Хорошо, но из обработки идет вызов функций которых нет в общих модулях, это сбивает с толку. Тогда переименовали бы в общем модуле функции в функции с префиксом HS256, а для RS256 сделали бы «заглушку» с комментариями. Или переименуйте вызываемые функции в самой обработке, удалите те, которых нет на самом деле. Да и было бы отлично, если привести готовый код для RS256 который вы использовали при работе со сторонней библиотекой, я думаю никто бы не огорчился, а только благодарны были бы. А то начинаешь сомневаться в том правильно ли все понял, отнимает время что бы разобраться что к чему и как правильно пользоваться.

    Reply
  24. frkbvfnjh

    В целом публикация хорошая. Написанная Вами библиотека функций очень нужная!

    Reply
  25. frkbvfnjh

    (22) Спасибо большое! Теперь все работает.

    Reply
  26. frkbvfnjh

    Все работает! Спасибо!

    Reply
  27. kloze

    Подскажите почему не работает. Сейчас HMAC получаю так

    <?php
    $p_sign = hash_hmac(‘sha1’, ‘811576.003RUB8290666011119https://www.test.ru2014052019110700003430’, pack(‘H*’, ‘3716699F804BE05E9E8D767ACEB6B88E’));
    echo $p_sign
    ?>
    

    HMAC = 7a619c6b8f79b95fc359a990075d6aeb2f6a6965

    А через эту обработку получает 9E83F1A022F0F916580132CA54639DA576E573BC

    Reply
  28. keypax

    (28)

    проверьте, пожалуйста здесь

    https://www.freeformatter.com/hmac-generator.html#ad-output

    Reply
  29. malikov_pro

    А можете ли Вы (автор) сделать реализацию pbkdf2? Тема Вам близка а реализации под 1C не нашел.

    Reply
  30. keypax

    (30) Я бы с удовольствием. К сожалению, не понимаю для каких практических задач нужен pbkdf2 в 1С. Например HMAC нужен был для интеграции с Twilio. И чтобы избавится от обращения к COM-объекту был реализован JWT и HMAC средствами платформы.

    Для каких реальных задач вы видите необходимость в реализации pbkdf2?

    Reply
  31. malikov_pro

    (31)

    1. Передача данных для проверки PIN кодов в удаленной системе (сейчас партнер передает Base64 строку с хешем солью и параметрами, из за отсутствия реализации проверяем через его API, сначала планировалось локально).

    2. Хранение паролей для внешних пользователей, куча решений когда внешних пользователей хранят как обычных. Я в решениях хранил в отдельном справочнике и по простому хешировал пароль.

    Reply
  32. keypax

    (32) По второму кейсу в простого хэша может быть и достаточно.

    По первому кейсу конечно нужна реализация pbkdf2.

    Не могу обещать, что сделаю в скором времени.

    Если руки дойдут – оповещу Вас.

    Reply
  33. maxx

    Добрый день! По поводу использования такого стандарта 1С. Если вот такой пример. Пусть надуманный, но недалеко от реального использования.

    Есть торговая сеть, состоящая из независимых организаций какого-нибудь торгового бренда, сети. Организации независимые (разные юридическое лица) и имеют свои базу 1С пусть «1С:Управление торговлей», но доработки у каждого свои , учитывающие своим местные особенности, свои программисты.

    Так как организации представляют один брэнд, они хотят при отсутствии товару у себя, посмотреть у своих коллег в других регионах. Так как единой общей базы нет, то решили сделать следующее: пользователи баз 1С могут зайти в личный веб-кабинет и там увидеть остатки других организаций в других регионах. Для этого используется технология http-запросов или soap-запросов, при этом конечно в самих базах 1С заведены пользователи только «свои», не чужих регионов.

    В своей родной «базе» 1С реализован механизм выдаче токена пользователю для веб-кабинета c правами на просмотр разной информации. Между базами 1С произведен обмен SecretKey. В каждой базе 1С реализовали веб-сервис, который получает Token и уже разобрав токен, понимается какие права данного пользователя, время токена и т.п.

    Возможно, такое использование?

    Reply
  34. keypax

    (34) Да, отличный пример!

    Благодаря JWT нет необходимости всем участникам сети делать в своих базах внешних пользователей.

    Я вижу процесс так:

    Участник сети выдает веб-кабинету свой SecretKey.

    Веб-кабинет с помощью SecretKey делает токен.

    Другой участник сети с помощью токена может сходить в базу первого участника сети за остатками.

    Первый участник сети по токену и SecretKey может определить выполнять запрос или нет.

    Токен не дает перманентный доступ — у него есть срок годности.

    Чтобы такую систему запустить нужно всем добавить в свои базы функционал:

    1. Генерация SecretKey и отправка его в Веб-кабинет

    2. HTTP-сервис, который умеет проверять токены и отдавать остатки

    Это можно реализовать через расширение.

    Ещё нужно сделать веб-кабинет, который фактически может не иметь веб-интерфейса. Достаточно сделать HTTP-сервис, который умеет принимать SecretKey и выдавать токены.

    Reply
  35. maxx

    (35) Один момент, который хотелось бы уточнить, нужно ли передавать веб-кабинету SecretKey, почему SecretKey нельзя передавать между базами 1С просто и там хранить?.

    А если веб-кабинет, сделан на чистом js (есть фрейворки сейчас даже таки) с запросами ajax, т.е. весь веб-часть это клиентская часть (т.е. там хранить SecretKey не надо)?

    Т.е. выдача токена в базе 1С с помощью SecretKey «базы 1», а в другой базе 1С «базы 2» проверка этого токена с помощью SecretKey, т.е. база1 обменялось с базой2 SecretKey (конечно, если SecretKey по-хорошему для каждой базы 1С надо делать свой, чтобы в случае проблем можно было быстро поменять SecretKey и ранее выданные токены перестали работать).

    Reply
  36. keypax

    (36) Можно так сделать как вы описываете, но тогда JWT теряет свой смысл, потому что все смогут бесконтрольно делать токены.

    Если токены выдавать централизовано, то можно ограничить количество токенов, чтобы пользователи не злоупотребляли возможностью видеть остатки у других пользователей.

    В токене ведь кроме срока его действия можно хоть конкретный код номенклатуры указать.

    Reply
  37. mcOrk

    А можно получить данную библиотеку? Не получилось найти на просторах интернета. Спасибо.

    Reply
  38. keypax

    (38) Да, конечно. В конце статьи есть кнопка «Скачать». Используйте, пожалуйста, её.

    Reply
  39. mcOrk

    (39) CF скачал, но я имел ввиду про стороннюю библиотеку, используемую Вами «AddIn.RSAUtils» Отдельной ссылки нигде не нашел.

    Reply
  40. keypax

    (40) по поводу AddIn.RSAUtils — напишите, мне пожалуйста на почту vasily@pintov.ru

    Reply

Leave a Comment

Ваш адрес email не будет опубликован. Обязательные поля помечены *