Сразу оговоримся: называть это полноценной двухфакторной аутентификацией можно токма на пол-фёдора. Запрос дополнительного секретного сообщения происходит уже после загрузки 1с. Это не по-понятиям, и в приличном обществе за такое бьют канделябром по лицу.
НО! Это уже на порядок лучше, чем файлик пароли.txt на рабочем столе сотрудника. Поэтому сегодня мы займёмся проектом «Абвер.Штирлиц» - сделаем свою систему подтверждения входа. Не эталон, но вы стеснялись, а я сделал.
Что у нас в арсенале:
-
Чтобы не обделять владельцев кнопочных печенек, добавим отправку секретного кода через Битрикс24. Но доверять этому каналу будем чутка меньше - если злоумышленник добрался до компуктера, то и Битрикс24 на нём найдёт.
-
На самый крайний случай дадим администраторам волшебную палочку - возможность генерировать одноразовые секретные слова. Воспользовался - выбросил.
Что нам понадобится (готовим заранее):
-
Чат-бот в Битрикс24. В этой статье //infostart.ru/1c/articles/2534278 мы его уже сделали (вы же, разумеется, взяли тот код, доработали напильником и сделали совсем-совсем правильным, потому что вы - молодец).
-
Бот в Telegram. Создаётся за пять минут через
@BotFather(инструкций в сети - миллион). Главное - не забудьте забрать у него токен, он нам пригодится.
Дисклеймер:
Все совпадения случайны, случайности не случайны, макарошки всегда стоят одинаково. В листингах кода иногда проскакивают англицизмы - это потому что в детстве я долго смотрел на сварку. А, как известно, если долго смотреть на сварку, то сварка начинает смотреть в тебя.
Приступаем к коду. Создаём расширение.
Назовём его «Проект Абвер.Штирлиц». Префикс объектов, естественно, будет «абвер».
1. Создаём константу-рубильник.
Первым делом добавим константу-переключатель, которая будет решать, включать ли нашу двухфакторную проверку. Вообще константы надо называть понятно. Всё надо называть понятно, потому что всё, что может быть понято не так, будет понято не так. В идеальном мире она называлась бы ДвухфакторнаяАутентификацияОбязательнаИИспользуетсяПовсеместноБезИсключений, но в нашем случае это:
абвер_РежимМаксимальнойПаранойи (тип Булево).
2. Создаём два общих модуля
-
абвер_ПолевойАгент- модуль На клиенте -
абвер_Штаб- модуль На сервере
// ===========================================================================
// ИнициироватьКонтрольнуюВстречу
//
// ОПЕРАЦИЯ: "ПЕРВАЯ ЛИНИЯ ОБОРОНЫ"
// ЦЕЛЬ: Проверить необходимость двухфакторной аутентификации и
// при положительном результате открыть контрольно-пропускной пункт
//
// ОСОБЕННОСТИ:
// - Функция является точкой входа в систему двухфакторной аутентификации
// - Открывает форму "абвер_КонтрольноПропускнойПункт"
// - Блокирует основной интерфейс до завершения проверки
//
// ===========================================================================
&НаКлиенте
процедура инициироватьКонтрольнуюВстречу() экспорт
если абвер_штаб.миссияАктивна() = ложь тогда
возврат;
конецЕсли;
_оповещение = новый ОписаниеОповещения("проверитьСостояниеЯвки", абвер_ПолевойАгент);
ОткрытьФорму("ОбщаяФорма.Абвер_КонтрольноПропускнойПункт",,,"Абвер_КонтрольноПропускнойПункт",,,_оповещение, РежимОткрытияОкнаФормы.БлокироватьВесьИнтерфейс);
конецПроцедуры
// ===========================================================================
// ПроверитьСостояниеЯвки
//
// ОПЕРАЦИЯ: "ДОПРОС С ПРИСТРАСТИЕМ"
// ЦЕЛЬ: Анализ результатов контрольной встречи и принятие решения
// о предоставлении доступа после закрытия формы КПП
//
// ===========================================================================
&НаКлиенте
процедура проверитьСостояниеЯвки(статусЯвки, Параметр) экспорт
если не статусЯвки = истина тогда
ЗавершитьРаботуСистемы(ложь, ложь);
конецЕсли;
конецПроцедуры
// ===========================================================================
// МиссияАктивна
//
// ОПЕРАЦИЯ: "СТОРОЖЕВАЯ БАШНЯ"
// ЦЕЛЬ: Определить активность системы двухфакторной аутентификации
//
// ВОЗВРАЩАЕТ:
// • ИСТИНА - Миссия активна, Абвер охраняет доступ
// • ЛОЖЬ - Миссия приостановлена, обычный режим доступа
// ===========================================================================
&НаСервере
Функция миссияАктивна() export
return Константы.абвер_РежимМаксимальнойПаранойи.Получить();
КонецФункции
// ===========================================================================
// ПередатьКодовоеСловоНаЯвку
//
// ОПЕРАЦИЯ: "КУРЬЕРСКАЯ СВЯЗЬ"
// ЦЕЛЬ: Сгенерировать и доставить одноразовую шифровку агенту
//
// ВОЗВРАЩАЕТ:
// • новую шифровку (строка)
//
// ОСОБЕННОСТИ:
// - Канал выбирается автоматически на основе уровня доверия к агенту
//
// ===========================================================================
&НаСервере
Функция ПередатьКодовоеСловоНаЯвку() export
новаяШифровка = СгенерироватьНовуюШифровку();
агент = ПараметрыСеанса.ТекущийПользователь;
если агент.абвер_ГрифДопуска = Перечисления.Абвер_ГрифыДопуска.СовершенноСекретно тогда
отправитьШифровкуЧерезканалТелеграм(агент, новаяШифровка);
иначе
отправитьШифровкуЧерезканалБитрикс(агент, новаяШифровка);
конецЕсли;
возврат новаяШифровка;
КонецФункции
// ===========================================================================
// ПолучитьChatId
//
// ОПЕРАЦИЯ: "ИДЕНТИФИКАЦИЯ РЕЗИДЕНТА"
// ЦЕЛЬ: Извлечь Telegram ID агента из дополнительных контактных данных
//
// ПАРАМЕТРЫ:
// • Агент - Пользователь, для которого ищем контакт
//
// ПРИМЕЧАНИЯ:
// • Telegram ID добавлен через форму пользователя как дополнительные контактные данные
//
// ВОЗВРАЩАЕТ:
// • Telegram ID в формате строки, если найден
// • Неопределено, если контакт не настроен
// ===========================================================================
Функция получитьChatId(агент) export
_вид = Справочники.ВидыКонтактнойИнформации.НайтиПонаименованию("Telegram ID");
агентОбъект = агент.ПолучитьОбъект();
for each i in агентОбъект.КонтактнаяИнформация do
if i.Вид = _вид then
return i.Представление;
break;
endIf;
endDo;
return undefined;
КонецФункции
// ===========================================================================
// ПолучитьChatId
//
// ОПЕРАЦИЯ: "ИДЕНТИФИКАЦИЯ РЕЗИДЕНТА"
// ЦЕЛЬ: ИзвлечьBitrix ID агента из дополнительных контактных данных
//
// ПАРАМЕТРЫ:
// • Агент - Пользователь, для которого ищем контакт
//
// ПРИМЕЧАНИЯ:
// • Bitrix ID добавлен через форму пользователя как дополнительные контактные данные.
// но лучше взять его из регистра соответсвий модуля коннектора, если он есть
//
// ВОЗВРАЩАЕТ:
// • Bitrix ID в формате строки, если найден
// • Неопределено, если контакт не настроен
// ===========================================================================
Функция получитьBitrixId(агент) export
_вид = Справочники.ВидыКонтактнойИнформации.НайтиПонаименованию("Bitrix ID");
агентОбъект = агент.ПолучитьОбъект();
for each i in агентОбъект.КонтактнаяИнформация do
if i.Вид = _вид then
return i.Представление;
break;
endIf;
endDo;
return undefined;
КонецФункции
// ===========================================================================
// ОтправитьШифровкуЧерезКаналТелеграм
//
// ОПЕРАЦИЯ: "СИГНАЛ ЧЕРЕЗ ЭФИР"
// ЦЕЛЬ: Доставить шифрованное сообщение агенту через Telegram API
//
// ПАРАМЕТРЫ:
// • Агент - Получатель сообщения
// • Шифровка - Кодовая фраза для отправки)
//
// ВОЗВРАЩАЕТ:
// • ИСТИНА - Успешно
// • ЛОЖЬ - Произошел сбой
// ===========================================================================
Функция отправитьШифровкуЧерезканалТелеграм(агент, шифровка)
_chatId = получитьChatId(агент);
if _chatId = undefined then
return false;
endIf;
АдресTelegramAPI = "api.telegram.org";
_httpConnect = new HTTPСоединение(АдресTelegramAPI,443,,,,,new ЗащищенноеСоединениеOpenSSL());
_token = "8239xxxxxxxxx-xxxxxxxxVLE";
_httpQuery = "bot" + _token + "/sendMessage?chat_id=" + СтрЗаменить(Формат(_chatId, "ЧДЦ=; ЧС=; ЧРГ=."), ".", "") + "&text=" + шифровка;
_q = new HTTPЗапрос(_httpQuery);
try
_res = _httpConnect.Получить(_q);
_readJSON = new ЧтениеJSON();
_readJSON.УстановитьСтроку(_res.ПолучитьТелоКакСтроку());
_data = ПрочитатьJSON(_readJSON);
_readJSON.Закрыть();
if _data.ok = true then
_rec = РегистрыСведений.Абвер_АрхивСжигаемыхШифровок.СоздатьМенеджерЗаписи();
_rec.шифровка = "шифровка";
_rec.chat_id = _data["result"]["chat"]["id"];
_rec.message_id = _data["result"]["message_id"];
_rec.срокДействия = ТекущаяДатаСеанса();
_rec.Записать();
endIf;
except
ВызватьИсключение "Конспиративная связь нарушена";
endTry;
if _res.КодСостояния = 200 then
return true;
endIf;
return false;
КонецФункции
// ===========================================================================
// ОтправитьШифровкуЧерезКаналБитрикс24
//
// ОПЕРАЦИЯ: "СЛУЖЕБНАЯ ДЕПЕША"
// ЦЕЛЬ: Доставить кодовое слово через корпоративный портал Битрикс24
//
// ПАРАМЕТРЫ:
// • Агент - Сотрудник-получатель (должен существовать в Битрикс24)
// • Шифровка - Кодовая фраза для доставки
//
// ВОЗВРАЩАЕТ:
// • ИСТИНА - Успешно
// • ЛОЖЬ - Произошел сбой
//
// ===========================================================================
Функция отправитьШифровкуЧерезканалБитрикс(агент, шифровка)
SSL = Новый ЗащищенноеСоединениеOpenSSL(новый СертификатКлиентаWindows(), Новый СертификатыУдостоверяющихЦентровОС());
Соединение = Новый HTTPСоединение("your.bitrix.url", 443,,,,5, SSL);
Заголовки = Новый Соответствие;
Заголовки.Вставить("Content-Type", "application/json");
Заголовки.Вставить("Access-Control-Request-Headers", "*");
IDПользователяПолучателя = получитьBitrixId(агент);
if IDПользователяПолучателя = undefined then
return false;
endIf;
Данные = Новый Структура();
Данные.Вставить("DIALOG_ID", IDПользователяПолучателя);
Данные.Вставить("CLIENT_ID", "awi7****************spq");
Данные.Вставить("BOT_ID", "***");
Данные.Вставить("MESSAGE", шифровка);
Данные.Вставить("SYSTEM", "Y");
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку();
ЗаписатьJSON(ЗаписьJSON, Данные);
ТелоЗапроса = ЗаписьJSON.Закрыть();
Запрос = Новый HTTPЗапрос("rest/26/h0yp*********bot.message.add.json", Заголовки);
Запрос.УстановитьТелоИзСтроки(ТелоЗапроса);
Ответ = Соединение.ВызватьHTTPМетод("POST", Запрос);
КонецФункции
// ===========================================================================
// ЗадокументироватьСжигаемуюШифровку
//
// ОПЕРАЦИЯ: "ОБЩИЙ ЗАПАС"
// ЦЕЛЬ: Создать универсальную одноразовую шифровку, доступную для
// использования ЛЮБЫМ агентом системы в течение ограниченного времени
//
// ===========================================================================
Функция задокументироватьСжигаемуюШифровку(шифровка) export
_rec = РегистрыСведений.Абвер_АрхивСжигаемыхШифровок.СоздатьМенеджерЗаписи();
_rec.шифровка = шифровка;
_rec.срокДействия = ТекущаяДатаСеанса() + 180;
_rec.агент = ПараметрыСеанса.ТекущийПользователь;
_rec.Записать();
конецФункции
// ===========================================================================
// СгенерироватьНовуюШифровку
//
// ОПЕРАЦИЯ: "ШИФРОВАЛЬНЫЙ ГЕНЕРАТОР"
// ЦЕЛЬ: Создать уникальную одноразовую шифровку для аутентификации
//
// ВОЗВРАЩАЕТ:
// • Строка с кодовой фразой (строка)
//
// АЛГОРИТМ:
// 1. Подбор слов из криптографического словаря Абвера
//
// ===========================================================================
Функция СгенерироватьНовуюШифровку() экспорт
Существительные = новый массив();
Существительные.add("Коалы");
Существительные.add("Тапиры");
Существительные.add("Нарвалы");
Существительные.add("Мандариновые утки");
Глаголы = новый массив();
Глаголы.add("программируют");
Глаголы.add("шифруют");
Глаголы.add("анализируют");
Глаголы.add("проектируют");
Глаголы.add("тестируют");
Глаголы.add("оптимизируют");
Дополнения = новый массив();
Дополнения.add("вслепую");
Дополнения.add("одной левой");
Дополнения.add("без страховки");
Дополнения.add("с благословения папы римского");
Дополнения.add("ради мирового господства");
ГенераторСлучайныхЧисел = Новый ГенераторСлучайныхЧисел();
ФразаМассив = новый массив;
ФразаМассив.add(Существительные[ГенераторСлучайныхЧисел.СлучайноеЧисло(0, Существительные.Количество()-1)]);
ФразаМассив.add(Глаголы[ГенераторСлучайныхЧисел.СлучайноеЧисло(0, Глаголы.Количество()-1)]);
ФразаМассив.add(Дополнения[ГенераторСлучайныхЧисел.СлучайноеЧисло(0, Дополнения.Количество()-1)]);
Фраза = СтрСоединить(ФразаМассив, " ");
Возврат Фраза;
КонецФункции
3. Ловим момент входа: обрабатываем ПриНачалеРаботыСистемы
Открываем Модуль приложения (самую главную процедурную) и создаем там событие перед ПриНачалеРаботыСистемы.
&Перед("ПриНачалеРаботыСистемы")
Процедура абвер_ПриНачалеРаботыСистемы()
абвер_ПолевойАгент.инициироватьКонтрольнуюВстречу();
КонецПроцедуры
4. Создаём перечисление «Грифы допуска» и привязываем к пользователям
Шаг 1: Заводим перечисление абвер_ГрифыДопуска с двумя значениями:
-
СовершенноСекретно (элита, избранные, те, кто прочёл инструкцию до конца)
-
ДляСлужебногоПользования (все остальные, кто пока на подхвате)
Мы будем привязывать этот гриф к пользователям. Так мы легко сможем разделять:
-
Кому отправлять код в Telegram (совсекретным)
-
Кому хватит Битрикс24 (для служебных)
Шаг 2: Цепляем гриф к пользователям
У Справочника пользователи добавляем новый реквизит:
-
Имя:
абвер_ГрифДопуска -
Тип:
ПеречислениеСсылка.абвер_ГрифыДопуска
Шаг 3: Выводим поле на форму (программно)
Обработчик ПриСозданииНаСервере для ФормыЭлемента Справочника Пользователи:
ПриСозданииНаСервере
5. Создаём общую форму ввода секретного слова
Создаём новую общую форму абвер_КонтрольноПропускнойПункт - наш главный барьер между пользователем и базой.
Что должно быть на форме:
-
ВведеннаяШифровка - Строка. На форме не размещаем
-
количествоПопытокОтправкиШифровки - Число, на форме не размещаем
-
НарушенияКонспирации - Число, на форме не размещаем
-
ОжидаемаяШифровка - Строка, размещаем на форме
-
Отозваться - Команда с действием Отозваться на Клиента.
-
ДекорацияОтправитьНовую - Декорация-надпись, гиперссылка. Действие - Нажатие - ДекорацияОтправитьНовуюНажатие
У меня получилась вот такая форма:

Код формы привёл ниже. Если в него присмотреться, можно заметить, что я добавил туда возможность прятать гиперссылку "Отправить новую шифровку". Для текущей статьи это, может, и излишне, но в остальном - прекрасный элемент взаимодействия с нашими любимыми пользователями.
6. Регистр сведений "абвер_АрхивСжигаемыхШифровок"
Создаём регистр сведений абвер_АрхивСжигаемыхШифровок для контроля шифровок. Структура - протокольная точность:
Измерения:
-
Шифровка (Строка) - сам код или секретное слово
Ресурсы:
-
СрокДействия (Дата и время) - когда шифровка самоуничтожается
-
ChatID (Строка) - идентификатор (чтобы потом удалить)
-
MessageID (Число) - идентификатор сообщения (чтобы потом удалить)
Реквизиты:
-
Агент - СправочникСсылка.Пользователи)
7. Обработка "абвер_КабинетКоменданта"
Создаём обработку для администраторов комендантов - инструмент, который генерирует одноразовые универсальные шифровки. Такую шифровку может использовать любой пользователь, но только один раз. Это как раз для староверов, у которых ни битрикса, ни telegram.
Визуально у меня получилось что-то вроде:

Код формы:
На этом всё. Запускаем 1с. Форму вы уже видели. Вот кодовая фраза в telegram.

Торжественно клянусь, что замышлял только шалость, но надеюсь, кому-то этот проект «Абвер.Штирлиц» пригодится - если не как готовое решение, то как вдохновение для своей паранойи.
Важное предупреждение:
Если вы решите внедрять это у себя - вы обязаны понять код, принять его всей душой и даже осознать. Каждую строчку. Потому что безопасность - это не про скопировал и понеслась.
Что я сознательно не сделал:
-
Регламентное задание для очистки архива - его нет в статье. Оно должно бегать и вычищать просроченные шифровки.
-
Хранение шифровок в открытом виде - никогда так не делайте. В реальной системе нужно хешировать или шифровать эти коды в базе. У меня в примерах они открыты только для наглядности. В бою - токма криптография.
Удачи, и да пребудет с вами паранойя разумной степени!
Вступайте в нашу телеграмм-группу Инфостарт