Реализация алгоритма TOTP-аутентификации на языке 1С

29.05.25

Администрирование - Информационная безопасность

Реализация алгоритма TOTP-аутентификации на языке 1С, включая создание QR-кода для добавления в приложения-аутентификаторы

Скачать файл

ВНИМАНИЕ: Файлы из Базы знаний - это исходный код разработки. Это примеры решения задач, шаблоны, заготовки, "строительные материалы" для учетной системы. Файлы ориентированы на специалистов 1С, которые могут разобраться в коде и оптимизировать программу для запуска в базе данных. Гарантии работоспособности нет. Возврата нет. Технической поддержки нет.

Наименование По подписке [?] Купить один файл
Реализация алгоритма TOTP-аутентификации на языке 1С
.epf 1,12Mb
0
0 Скачать (1 SM) Купить за 1 850 руб.

Вступление

В процессе повышения уровня безопасности в организации и повсеместном внедрении двухфакторной аутентификации нам понадобилось реализовать свой генератор TOTP-кодов, т.к. имеющееся решение им не обладало. В Базе знаний нашлось несколько публикаций, в них были вполне работающие решения, но без пояснений, почему сделано именно так. Поэтому я решил написать полноценный гайд по реализации, чтобы им смог воспользоваться разработчик любого уровня.

Отмечу, что в базе Инфостарта есть готовая обработка реализации этого алгоритма. Там код длиннее, но зато работает на старых версиях платформы и не привязан к БСП. Если вам нужен сразу готовый код - берите ее. Если хотите понять, как работает TOTP-алгоритм и как написать самостоятельно - читайте дальше.

 

Немного терминологии

OTP - One-time Password - алгоритм формирования одноразовых паролей. Изначальной версией реализации был HOTP - Hash-based Message Authentication Code (HMAC) OTP, но потом его сменил TOTP, который мы и будем реализовывать. Приложения-аутентификаторы, тем не менее, обычно поддерживают оба. Это варианты "По времени" и "По счетчику".

TOTP или Time-based One-time Password - алгоритм формирования одноразового кода на основании текущего времени. Суть его неплохо (но недостаточно хорошо для целей этой статьи) описана на википедии и тоже википедия, но конкретно про Google Authenticator, а также есть статья на Хабре
Как я писал выше, этот алгоритм реализован во всех аутентификаторах (Google Authenticator, BitrixOTP, Microsoft Authenticator, Twilio Authy и т.д.), именуется как алгоритм "По времени", при одинаковых исходных данных во всех реализациях выдается одинаковый результат.

HMAC или Hash-based Message Authentication Code - алгоритм, используемый внутри алгоритма OTP. С технической стороны он обеспечивает неизменность исходного значения при передаче его в ненадежной среде. Но для наших целей нам интересно другое замечательное его свойство - при использовании хеш-функции SHA-1 на выходе она всегда дает двадцатибайтовое значение при использовании. Техническое описание, опять же, доступно на вики.

Если пока вы ловите себя на мысли, что в прочитанном выше тексте все слова по отдельности понятны, но общий смысл упорно ускользает - не переживайте, дальше все будет просто. Хоть и не очень лаконично...

 

Реализация алгоритма

Итак, алгоритм TOTP (буду описывать именно Time-based вариант) по пунктам:

1. Создаем какой-то строковый секретный ключ. Можно использовать любое значение, включая пустую строку. Это будет первый параметр для функции HMAC (не паникуем от страшных аббревиатур, читаем дальше). Для каждого ключа на выходе алгоритма будет свой шестизначный код.

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

СекретныйКлюч = "infostart";

 

2. Формируем данные от текущего времени - второй параметр HMAC. Я буду для наглядности использовать конкретные момент времени - 28 мая 2025 года, 17:05, UTC+5. В рабочей реализации нужно брать текущее время.
Но алгоритм требует не само время, а число. И вычислять его тоже предлагает вполне конкретным способом - это количество секунд от начала UNIX-эры по UTC, поделенное на 30.

// Для произвольной даты
ПроверяемаяДата = Дата(2025, 5, 28, 17, 5, 0);
ДанныеВремени = Цел((УниверсальноеВремя(ПроверяемаяДата) - Дата(1970, 1, 1)) / 30);

// Для текущей даты можно попроще
ДанныеВремени = Цел((ТекущаяУниверсальнаяДата() - Дата(1970, 1, 1)) / 30);

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

Результат для используемого в примере значения - 58 281 130.

Но давайте проверим это независимым источником - онлайн-конвертером. Туда, естественно, нужно вводить время UTC, т.е. на пять часов меньшее, чем то, что взял в примере.



Конвертер говорит, что получается 1748433901. Делим это на 30, получаем 58 281 130,0333333333. Если избавить этот результат от дробной части, он совпадет с вышеуказанным.

 

3. Вызываем функцию HMAC с этими параметрами, используя хеш-функцию SHA-1.
Реализация этой функции есть в БСП (во всяком случае, в версии 3.1.10.467, которая у меня): РаботаВМоделиСервисаБТС.HMAC(Ключ, Данные, Тип, РазмерБлока), но, к сожалению, она не экспортная. Поэтому либо забираете ее целиком из модуля, либо делаете расширение, либо используете готовую реализацию из этой статьи Инфостарта. В своей реализации я скопировал БСП-функцию в свой модуль (вместе с парой небольших служебных функций), так что в примере буду обращаться к ней напрямую.

Но просто так отдать туда параметры нельзя, т.к. функция принимает только двоичные данные. Для корректного результата их нужно передать туда вполне определенным образом. И возвращает она тоже двоичные данные, а нам, забегая вперед, скажу, что неплохо бы получить шестнадцатеричную строку. Поэтому код будет следующий:

СекретныйКлючДляHMAC = ПолучитьДвоичныеДанныеИзСтроки(СекретныйКлюч);

Буфер = Новый БуферДвоичныхДанных(8, ПорядокБайтов.BigEndian);
Буфер.ЗаписатьЦелое64(0, ДанныеВремени);
ДанныеВремениДляHMAC = ПолучитьДвоичныеДанныеИзHexСтроки(ПолучитьHexСтрокуИзБуфераДвоичныхДанных(Буфер));
		
РезультатХеширования = HMAC(СекретныйКлючДляHMAC, ДанныеВремениДляHMAC, ХешФункция.SHA1, 64);
РезультатHMAC = ПолучитьHexСтрокуИзДвоичныхДанных(РезультатХеширования);

Третьим параметром мы отдаем хеш-функцию SHA-1, как того требует алгоритм, четвертый параметр - длина блока - 64 - тоже такой именно по условиям алгоритма HMAC (подробнее, почему именно так, можно узнать в описании, ссылку я приводил в разделе терминологии). Результат преобразовываем в HEX-строку.

Итого на выходе у нас "1EEA36ABFDCE884076EB770C180402A5016A424F". 40 символов в 16-ричной кодировке, они же 20 байт, как нам того требовалось.

Конечно же, нужно перепроверить себя сторонним конвертером:


 

Если неочевидно, откуда взялось 0000000003794CAA, которое я подставил в поле Input Content, то это шестнадцатеричное представление числа 58281130, полученного на предыдущем этапе. И оно же - результат функции ПолучитьHexСтрокуИзБуфераДвоичныхДанных(Буфер), который потом в виде двоичных данных отдается в функцию HMAC().

 

4. Дальше в описании алгоритма присутствует такой кусок: вычислить смещение, взять последний полубайт и преобразовать его в число. У нас шестнадцатеричная строка, каждый байт - два символа, а следовательно полубайт - один символ, и не абы какой, а который может быть строго в диапазоне 0-F.

Смещение = ЧислоИзШестнадцатеричнойСтроки("0x" + Прав(РезультатHMAC, 1));

Код при этом совсем простой, если знать, что с версии 8.3.10 во встроенном языке есть функция ЧислоИзШестнадцатеричнойСтроки(). Но если хотите вспомнить школьные занятия по переводу чисел из одной системы счисления в другую - можете поупражняться.

Результат переменной Смещение на этом этапе - 15.

 

5. Следующий шаг алгоритма - взять 4 байта с указанного смещения. Для понимания, что такое смещение - это порядковый номер от начала строки. Только надо помнить, что во-первых, речь о порядковом номере байта (которых в строке 20 и каждый их которых представлен двумя символами), а во-вторых, смещением первого байта считается 0. Т.е. для полученного ранее значения переменной РезультатHMAC значение со смещением 0 будет "1EEA36AB", т.е. символы с 1-го по 8-й. Для результата 15 нам нужно взять символы с 31-го по 38-й. И снова очень простой код:

ЧастьХеша = Сред(РезультатHMAC, Смещение * 2 + 1, 8);

На текущем этапе в переменной ЧастьХеша находится значение "A5016A42".

 

6. Следующим этапом нужно в этом значении (а точнее в его двоичном представлении) сбросить первый бит. Реализацию опять провернем, пользуясь представлением байта как двух символов в шестнадцатеричной системе. Возьмем первый полубайт (а это первый символ), первый бит будет 1, если это число не меньше 8. Т.е. 8 - это '1000', 9 - '1001', а наш красивый символ A - '1010'. И получить нам нужно для 8-ки - '0000' (т.е. 0), для 9-ки - '0001' (т.е. 1), для A - '0010' (т.е. 2).

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

Теперь опять вспомним про функцию ЧислоИзШестнадцатеричнойСтроки(), которая за нас сделает всю грязную работу:

// Взяли первый символ и преобразовали его в десятичную систему счисления
ПервыйПолубайт = ЧислоИзШестнадцатеричнойСтроки("0x" + Лев(ЧастьХеша, 1));

Если ПервыйПолубайт >= 8 Тогда
	ЧастьХеша = Формат(ПервыйПолубайт - 8, "ЧН=") // Вычли 8, тем самым очистив первый бит
				 + Сред(ЧастьХеша, 2);            // и дописали остаток
КонецЕсли;

Переменная ЧастьХеша после этих преобразований имеет значение "25016A42". Поменялся первый символ.

 

7. Ну и финальная часть генерации кода - получение шестизначного числа. Снова воспользуемся преобразованием в число из шестнадцатеричной строки. Применив его к переменной ЧастьХеша, получим числовое значение, от которого возьмем последние 6 цифр. Это и будет нужный нам код.

Код = Формат(ЧислоИзШестнадцатеричнойСтроки("0x" + ЧастьХеша) % 1000000, "ЧЦ=6; ЧВН=");

На всякий случай, поясню, что функция % возвращает остаток от деления. И остаток от деления на миллион - это последние шесть цифр числа. А Формат() с параметром "ЧЦ=6; ЧВН=" вернет число длиной 6 знаков с ведущими нулями, т.е. 5 преобразует в 000005. Также можно добавить параметр "ЧГ=", чтобы убрать разделители разрядов.

Результат на этом этапе - "849 730". Это и есть нужный нам код аутентификации.

 

Код всего алгоритма для боевого использования, т.е. для текущей даты:

СекретныйКлюч = "infostart";                                                                       

ДанныеВремени = Цел((ТекущаяУниверсальнаяДата() - Дата(1970, 1, 1)) / 30);

СекретныйКлючДляHMAC = ПолучитьДвоичныеДанныеИзСтроки(СекретныйКлюч);

Буфер = Новый БуферДвоичныхДанных(8, ПорядокБайтов.BigEndian);
Буфер.ЗаписатьЦелое64(0, ДанныеВремени);
ДанныеВремениДляHMAC = ПолучитьДвоичныеДанныеИзHexСтроки(ПолучитьHexСтрокуИзБуфераДвоичныхДанных(Буфер));
		
РезультатHMAC = HMAC(СекретныйКлючДляHMAC, ДанныеВремениДляHMAC, ХешФункция.SHA1, 64);
РезультатHMAC = ПолучитьHexСтрокуИзДвоичныхДанных(РезультатHMAC);

Смещение = ЧислоИзШестнадцатеричнойСтроки("0x" + Прав(РезультатHMAC, 1));

ЧастьХеша = Сред(РезультатHMAC, Смещение * 2 + 1, 8);

ПервыйПолубайт = ЧислоИзШестнадцатеричнойСтроки("0x" + Лев(ЧастьХеша, 1));
Если ПервыйПолубайт >= 8 Тогда
	ЧастьХеша = Формат(ПервыйПолубайт - 8, "ЧН=") + Сред(ЧастьХеша, 2);
КонецЕсли;

Код = Формат(ЧислоИзШестнадцатеричнойСтроки("0x" + ЧастьХеша) % 1000000, "ЧЦ=6; ЧВН=");

 

8. Да, это еще не все. Нам еще надо уметь отдавать секретный код в сторонний аутентификатор. Если мы пойдем в любой из них и введем слово "infostart" как ключ настройки, то получим ошибку "Слишком короткое значение ключа". Что же это значит - что нам надо просто взять код подлиннее? Нет. Приложения аутентификации принимают не сам код, а его представление в кодировке Base32. Если воспользоваться очередным онлайн-конвертером, мы увидим, что представление будет NFXGM33TORQXE5A=. Именно со знаком = на конце. Его и нужно отдавать приложению.

Однако в процессе работы с разными приложениями на разных ОС мы выяснили, что аутентификаторы на iOS этот знак равно (а иногда их может быть несколько - до семи штук включительно) на дух не переваривают. Но спокойно принимают значение, если знаки = опустить. А версии на Android принимают и так, и так.

Конечно, мы проверили не все возможные атуентификаторы. Да это и не требуется. Главное, что мы выяснили, что в нашей реализации знаки равно лучше опустить.

Теперь, если вы не поленитесь ввести NFXGM33TORQXE5A в качестве ключа настройки, вы получите тот же результат, что выдаст мой алгоритм. Можно проверить на онлайн-аутентификаторе, где не придется набирать его вручную. Заодно сможете убедиться, что коды NFXGM33TORQXE5A и NFXGM33TORQXE5A= дают одинаковый результат.

 

9. Теперь про формат Base32. Во встроенном языке 1С нет его реализации на текущий момент, есть только Base64, которым заменить не выйдет. Поэтому пришлось написать свою реализацию, которую я приведу без пояснений. Желающие могут прочитать несложный мануал и попробовать написать самостоятельно, результат проверить вышеупомянутым онлайн-конвертером.

Функция СтрокаВСтроку32(Строка, ДополнятьСпецсимволами = Истина) Экспорт
	
	Алфавит = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
	
	БитоваяСтрока = "";
	Для НомерСимвола = 1 По СтрДлина(Строка) Цикл
		ЧислоСимвола = КодСимвола(Сред(Строка, НомерСимвола, 1)); // Код символа числом
		Для Степень = 0 По 7 Цикл // Преобразование в биты
			Разрядность = Pow(2, 7 - Степень);
			Бит = Цел(ЧислоСимвола / Разрядность);
			ЧислоСимвола = ЧислоСимвола - Разрядность * Бит;
			БитоваяСтрока = БитоваяСтрока + Бит;
		КонецЦикла;
	КонецЦикла;
	
	Результат = "";
	Пока СтрДлина(БитоваяСтрока) > 0 Цикл
		НомерСимвола = ЧислоИзДвоичнойСтроки("0b" + Лев(Лев(БитоваяСтрока, 5) + "0000", 5));
		БитоваяСтрока = Сред(БитоваяСтрока, 6);
		Результат = Результат + Сред(Алфавит, НомерСимвола + 1, 1);
	КонецЦикла;
	
	Пока ДополнятьСпецсимволами И СтрДлина(Результат) % 8 <> 0 Цикл
		Результат = Результат + "=";
	КонецЦикла;
	
	Возврат Результат;
	
КонецФункции	

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

 

10. Кажется, это последний пункт. Приложения-аутентификаторы умеют считывать QR-коды, так почему бы вместо страшной строки в формате Base32 не отдавать пользователю QR-код? Тем более, что БСП умеет их создавать функцией ГенерацияШтрихкода.ДанныеQRКода().

В качестве данных QR-код мы должны отдать строку вида "otpauth://totp/тут мое название в списке?secret=СекретныйключВФорматеBase32". Т.е. мы сразу можем зашить в него то наименование, под которым оно отобразится в приложении. В своей реализации мы зашили туда адрес ресурса, при входе в который требуется наша аутентификация.

Остальные параметры функции ДанныеQRКода() достаточно подробно описаны в комментариях к ней, подробно останавливаться на них не вижу смысла. Возвращает она двоичные данные картинки, которые вы можете вывести куда вам заблагорассудится. Вот пример вывода в табличный документ:

ДанныеQRКода = ГенерацияШтрихкода.ДанныеQRКода("otpauth://totp/" + АдресСервиса + "?secret=" + СекретныйКлюч32, 0, 80);
	
Рисунок = ТабличныйДокумент.Рисунки.Добавить(ТипРисункаТабличногоДокумента.Картинка);
Рисунок.Картинка = Новый Картинка(ДанныеQRКода);
Рисунок.Расположить(ТабличныйДокумент.Область(1, 1));
Рисунок.РазмерКартинки = РазмерКартинки.АвтоРазмер;

 

Заключение

Если вы добрались до этого места и у вас все хорошо - я вас поздравляю! Теперь у вас есть все, чтобы написать и свое приложение для аутентификации, и реализовать проверку введенного кода на сервере. Для безудержно ленивых прикладываю обработку, в которой есть все вышеперечисленное. Поставляется как есть, без гарантии поддержки и консультаций, учитывайте это, когда потратите свои кровные 1sm.


Проверено на следующих конфигурациях и релизах:

  • 1С:Библиотека стандартных подсистем, редакция 3.1, релизы 3.1.10.467

TOTP Time-based OTP аутентификатор authenticator

См. также

Информационная безопасность Пароли Платформа 1С v8.3 Бесплатно (free)

Все еще храните пароли в базе? Тогда мы идем к вам! Безопасное и надежное хранение секретов. JWT авторизация. Удобный интерфейс. Демо конфигурация. Бесплатно.

30.05.2024    8799    kamisov    19    

63

Математика и алгоритмы Инструментарий разработчика Программист Платформа 1С v8.3 Мобильная платформа Россия Абонемент ($m)

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

1 стартмани

09.06.2023    15682    8    SpaceOfMyHead    20    

63

Информационная безопасность Платформа 1С v8.3 1C:Бухгалтерия Бесплатно (free)

От клиента клиенту, от одной системы к другой, мы вновь и вновь встречаем одни и те же проблемы и дыры в безопасности. На конференции Infostart Event 2021 Post-Apocalypse Виталий Онянов рассказал о базовых принципах безопасности информационных систем и представил чек-лист, с помощью которого вы сможете проверить свою систему на уязвимость.

26.10.2022    12063    Tavalik    46    

118

Информационная безопасность Пароли Системный администратор Программист Платформа 1С v8.3 1C:Бухгалтерия Абонемент ($m)

Недавно в 1С появилась возможность двухфакторной аутентификации. Пример такой аутентификации можно увидеть при входе в клиент-банках (когда вначале пользователь вводит логин-пароль, а затем ему прилетает смс). Как это все настроить, мы и разберем. Кроме настройки авторизации приложил простенькую конфигурацию с http-сервисом и telegram-ботом, который будет присылать коды доступа.

1 стартмани

18.05.2022    18318    97    vov4ik1212    35    

67

Информационная безопасность Системный администратор Программист Бесплатно (free)

HTTP-сервисы ускоряют и упрощают разработку обмена данными между 1С и другими приложениями. Но нельзя забывать, что HTTP-сервис – это дверь в информационную систему. О том, как обеспечить безопасность HTTP-сервиса и не оставить лазеек злоумышленникам, на митапе «Безопасность в 1С» рассказал заместитель начальника отдела разработки ГКО PRO Дмитрий Сидоренко.

11.05.2022    11684    dsdred    14    

66

Информационная безопасность Программист Платформа 1С v8.3 Бесплатно (free)

Есть такой стандарт «Безопасность прикладного программного интерфейса сервера». Многие его читали. Кто-то даже понимает то, что там написано. Но, как показывает практика, его мало кто соблюдает. Чем грозит отступление от этого стандарта? В чем опасность общих модулей с признаком «Вызов сервера»? На эти вопросы на митапе «Безопасность в 1С» ответил разработчик рекомендательных систем Владимир Бондаревский.

02.03.2022    6602    bonv    12    

69
Комментарии
Подписаться на ответы Инфостарт бот Сортировка: Древо развёрнутое
Свернуть все
1. SerVer1C 914 29.05.25 14:53 Сейчас в теме
В моём 2FA более полная реализация Base32 (выдернута из питоновского алгоритма), которая учитывает всю чехарду с символами "=". Ну и не зависит от каких-либо БСП. Всё кодилось с нуля. Даже клиентский SHA-1 можно взять из соседней публикации.
Оставьте свое сообщение