Как зашифровать одну процедуру 1С и привязать её к ИНН покупателя
Администрирование - Информационная безопасность
Знакомая история: вы написали процедуру, в которой вся ценность, - свою формулу, коэффициенты, алгоритм, - вложили её в обработку и продали одному клиенту. А через месяц встречаете эту обработку у трёх других. Файл .epf копируется мышкой, и остановить это договором трудно. Хочется, чтобы та самая процедура работала только у того, кто её купил.
Первое, что приходит в голову, - проверить в коде ИНН организации и выйти, если он чужой. Я покажу, почему это вообще не защита, как вместо неё зашифровать тело одной процедуры и привязать его к ИНН покупателя, и, честно, где у такой защиты потолок. Обработка на входе берёт тело процедуры, а на выходе отдаёт готовый кусок кода для вставки в вашу обработку. Весь механизм - на чистом встроенном языке, без СЛК, без внешних компонент и без покупки ключей.
Почему проверка ИНН в коде не защищает
Самый частый совет на форумах звучит так:
Если Организация.ИНН <> "600900529130" Тогда Возврат; // чужая база - выходим КонецЕсли; // ... дальше полезная логика ...
Полезная логика лежит рядом, открытым текстом. Тот, кто захочет снять привязку, открывает модуль, находит это Если и удаляет три строки. Код под ним никуда не делся, он просто начинает выполняться у всех. Пароль на модуль тут помогает слабо: его снимают теми же общедоступными средствами, которыми снимают пароль с любой внешней обработки.
Проверка ИНН - это замок, повешенный на дверь рядом с открытым окном. Главную ценность, тело вашей процедуры с логикой и коэффициентами, она не прячет вовсе: его видно в модуле сразу.
Что меняет шифрование тела процедуры
Тело процедуры - сам алгоритм, коэффициенты, любой текст кода - храним в обработке в зашифрованном виде. Ключ не лежит в файле: он выводится из ИНН покупателя и секрета разработчика. На верном ИНН ключ собирается правильно, тело расшифровывается и выполняется. На чужом ИНН ключ другой, и расшифровки нет - выполнять нечего.
Честно сразу о границе, чтобы дальше читать с правильными ожиданиями. Шифрование решает одну задачу: конфиденциальность. Конкурент, которому попал файл, видит шифроблок; тело процедуры закрыто, и просто так его не прочитать. Это ровно то, чего проверка ИНН не даёт никогда. А вот жёсткой привязки к одному покупателю шифрование само по себе не обеспечивает, и ниже я покажу, почему. Это не DRM и не замок на тираж, это способ не отдавать свою процедуру в открытом виде.
Весь механизм на одной схеме: слева разработчик шифрует тело процедуры под ИНН и получает готовый блок, справа этот блок встаёт в боевую обработку и расшифровывает процедуру под ИНН базы покупателя. Дальше разберу каждый шаг.
Чем шифровать на встроенном языке
Штатного симметричного шифра вроде AES в платформе нет. Зато в любой современной версии есть ХешированиеДанных с SHA-256, и этого достаточно, чтобы собрать потоковый шифр в режиме счётчика. Схема собирается из трёх понятных кусков.
Ключ из ИНН. Мастер-ключ = SHA256(ИНН + "|" + секрет), 32 байта. Из него я вывожу два отдельных ключа: один для шифрования, другой для тега подлинности. Разделять их - дешёвая гигиена: один и тот же ключ не должен работать в двух ролях.
Функция КлючИзИНН(ИНН, Секрет) Хэш = Новый ХешированиеДанных(ХешФункция.SHA256); Хэш.Добавить(ПолучитьДвоичныеДанныеИзСтроки(СокрЛП(ИНН) + "|" + Секрет, "UTF-8")); Возврат Хэш.ХешСумма; КонецФункции // Производный ключ = HMAC-SHA256( мастер-ключ, метка ). Функция Подключ(МастерКлючДД, Метка) Возврат HMAC_SHA256(МастерКлючДД, ПолучитьДвоичныеДанныеИзСтроки(Метка, "UTF-8")); КонецФункции
Гамма с нонсом. Гамму (псевдослучайный поток для XOR) я вырабатываю по блокам: блок i = HMAC-SHA256(ключ шифра, версия + нонс + счётчик i). Нонс - это 32 случайных символа, свои на каждую упаковку. Без него возникает классическая дыра потокового шифра: если упаковать тем же ключом два текста, XOR двух шифроблоков равен XOR открытых текстов, и, зная один, восстанавливаешь второй без всякого ключа. Для тиражного решения это реальный сценарий - вы переупаковываете обновление процедуры тому же покупателю. Нонс эту дыру закрывает: тот же ключ, другой нонс, другая гамма.
Счётчик блока я кладу четырьмя байтами, а не через Строка(ИндексБлока). Строковое представление числа зависит от региональных настроек: 1000 в одной базе даёт "1000", в другой - "1 000" с неразрывным пробелом. Если упаковать в одной базе, а распаковать в другой, гамма разойдётся после блока 1000, и MAC этого не заметит - он проверяет нонс и шифртекст, а не способ построения счётчика. Четыре байта big-endian от локали не зависят.
Функция Гаммировать(ВходБуф, КлючШифраДД, НонсДД) РазмерБлока = 32; Длина = ВходБуф.Размер; ВыходБуф = Новый БуферДвоичныхДанных(Длина); ОснованиеДД = СоединитьДД(ВерсияДД(), НонсДД); ГаммаБуф = Неопределено; Для Сч = 0 По Длина - 1 Цикл ИндексБлока = Цел(Сч / РазмерБлока); ПозицияВБлоке = Сч - ИндексБлока * РазмерБлока; Если ПозицияВБлоке = 0 Тогда Сообщение = СоединитьДД(ОснованиеДД, СчётчикДД(ИндексБлока)); ГаммаБуф = ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(HMAC_SHA256(КлючШифраДД, Сообщение)); КонецЕсли; ВыходБуф.Установить(Сч, ПобитовоеИсключительноеИли(ВходБуф.Получить(Сч), ГаммаБуф.Получить(ПозицияВБлоке))); КонецЦикла; Возврат ВыходБуф; КонецФункции Функция СчётчикДД(Индекс) Буф = Новый БуферДвоичныхДанных(4); Буф.Установить(0, Цел(Индекс / 16777216) % 256); Буф.Установить(1, Цел(Индекс / 65536) % 256); Буф.Установить(2, Цел(Индекс / 256) % 256); Буф.Установить(3, Индекс % 256); Возврат ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(Буф); КонецФункции
Нюанс платформы: у БуферДвоичныхДанных размер читается свойством Размер, а не Количество, и ошибка вылезает только в рантайме. Побитовые функции в модуле объекта внешней обработки доступны, если база не в режиме совместимости ниже 8.3.11.
Тег подлинности. Чтобы отличить "расшифровалось правильно" от "получился мусор под чужим ИНН", к пакету прикладывается тег: HMAC-SHA256(ключ тега, версия + нонс + шифртекст), проверяется до расшифровки (encrypt-then-MAC). Нонс всегда ровно 32 символа, и это важно: распаковщик сначала проверяет длину и hex-формат нонса, поэтому границы полей в сообщении HMAC однозначны, и никто не "перельёт" байт из нонса в шифртекст, оставив тег прежним. HMAC здесь не для красоты: голый SHA256(ключ + текст) уязвим к удлинению сообщения, HMAC по RFC 2104 этого класса проблем лишён. Полная реализация HMAC на ХешированиеДанных - в демо-обработке; копируется вместе со вспомогательной функцией склейки двоичных данных.
Упаковка и распаковка
Функция Зашифровать(Текст, ИНН, Секрет) Экспорт МастерКлюч = КлючИзИНН(ИНН, Секрет); КлючШифра = Подключ(МастерКлюч, "enc"); КлючТега = Подключ(МастерКлюч, "mac"); Нонс = НовыйНонс(); НонсДД = ПолучитьДвоичныеДанныеИзСтроки(Нонс, "UTF-8"); ОткрытыйБуф = ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(ПолучитьДвоичныеДанныеИзСтроки(Текст, "UTF-8")); ШифрБуф = Гаммировать(ОткрытыйБуф, КлючШифра, НонсДД); ШифрДД = ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(ШифрБуф); Результат = Новый Структура; Результат.Вставить("Нонс", Нонс); Результат.Вставить("Шифр", Base64Строка(ШифрДД)); Результат.Вставить("Тег", Base64Строка(ВычислитьТег(КлючТега, НонсДД, ШифрДД))); Возврат Результат; КонецФункции Функция Расшифровать(Шифр64, Тег, Нонс, ИНН, Секрет) Экспорт Результат = Новый Структура("Успех, Текст, Сообщение", Ложь, "", ""); Попытка ШифрДД = Base64Значение(Шифр64); ТегОжидаемыйДД = Base64Значение(СокрЛП(Тег)); Исключение Результат.Сообщение = "Шифроблок или тег не распознаны как Base64."; Возврат Результат; КонецПопытки; НонсЧист = СокрЛП(Нонс); Если НЕ НонсКорректен(НонсЧист) Тогда // ровно 32 hex-символа Результат.Сообщение = "Нонс повреждён: нужно ровно 32 hex-символа."; Возврат Результат; КонецЕсли; МастерКлюч = КлючИзИНН(ИНН, Секрет); КлючТега = Подключ(МастерКлюч, "mac"); НонсДД = ПолучитьДвоичныеДанныеИзСтроки(НонсЧист, "UTF-8"); Если Base64Строка(ВычислитьТег(КлючТега, НонсДД, ШифрДД)) <> Base64Строка(ТегОжидаемыйДД) Тогда Результат.Сообщение = "ИНН не совпал: доступ закрыт."; Возврат Результат; КонецЕсли; КлючШифра = Подключ(МастерКлюч, "enc"); ОткрытыйБуф = Гаммировать(ПолучитьБуферДвоичныхДанныхИзДвоичныхДанных(ШифрДД), КлючШифра, НонсДД); Результат.Успех = Истина; Результат.Текст = ПолучитьСтрокуИзДвоичныхДанных(ПолучитьДвоичныеДанныеИзБуфераДвоичныхДанных(ОткрытыйБуф), "UTF-8"); Возврат Результат; КонецФункции
Пакет - это три поля: нонс, шифр, тег. Они передаются вместе, шифроблок без своего нонса не расшифровать. Проверка идёт через тег, минуя попытку привести расшифрованные байты к строке: на чужом ИНН мусорные байты могли бы и не быть валидным UTF-8, тег закрывает вопрос раньше.
Готовый блок для вставки: функция КодПодИНН
Три поля руками переносить неудобно, поэтому обработка собирает их в готовую функцию. Вводите тело процедуры (оно работает с переменными Вход и Результат), ИНН покупателя и секрет, жмёте "Собрать блок" - и получаете кусок кода, который копируется в модуль вашей обработки целиком:
Функция КодПодИНН() Нонс = "bb44b650d3244f529e1ec63ad62383a5"; Шифр64 = "pagVRLscOUDULgqcb7qVTvdzWEhO4V5mX/k0cLKpZH9Bjp+cpSjkL/mwqWtViV8PPw=="; Тег64 = "j5YD1t7pZnPaMzJonwdZs6EHm75NCIGhL8hS7qRhkMg="; Секрет = "ваш-секрет"; // держите секрет у себя Пакет = Расшифровать(Шифр64, Тег64, Нонс, ИННПервойОрганизации(), Секрет); Если НЕ Пакет.Успех Тогда ВызватьИсключение "Экземпляр выдан другой организации."; КонецЕсли; Возврат Пакет.Текст; КонецФункции
Функции расшифровки (ключ из ИНН, HMAC, гаммирование) копируются из демо-обработки один раз - код открыт. После этого ваша процедура у покупателя становится тонкой: в ней нет ни алгоритма, ни коэффициентов, только вызов.
Функция Наценка(Вход) Экспорт Результат = 0; Выполнить(КодПодИНН()); // тело расшифровывается под ИНН базы и выполняется здесь Возврат Результат; КонецФункции
КодПодИНН читает ИНН текущей базы, расшифровывает тело и отдаёт его на Выполнить. Тело работает с теми же Вход и Результат, что вы задали при упаковке. Совпал ИНН - процедура считает как считала, не совпал - Расшифровать не проходит, и обработка честно отказывает. В модуле при этом не видно ни формулы, ни чисел: только шифроблок.
Где взять ИНН в базе покупателя
ИНН можно подставить из базы, чтобы не вводить руками. Но честная оговорка: ВЫБРАТЬ ПЕРВЫЕ 1 без порядка возвращает произвольную подходящую организацию, не обязательно основную. В базе с несколькими юрлицами это не "основная организация", и в прикладном коде организацию надо выбирать явно. Для демо кнопка так и называется - "Взять ИНН первой организации", и форма предупреждает, что результат стоит проверить глазами.
Функция ИННПервойОрганизации() Экспорт Попытка Если Метаданные.Справочники.Найти("Организации") = Неопределено Тогда Возврат ""; КонецЕсли; Запрос = Новый Запрос; Запрос.Текст = "ВЫБРАТЬ ПЕРВЫЕ 1 ИНН КАК ИНН |ИЗ Справочник.Организации |ГДЕ НЕ ПометкаУдаления И ИНН <> """""; Выборка = Запрос.Выполнить().Выбрать(); Если Выборка.Следующий() Тогда Возврат СокрЛП(Выборка.ИНН); КонецЕсли; Исключение Возврат ""; КонецПопытки; Возврат ""; КонецФункции
Что показал прогон на стенде
Собрал демо-обработку и прогнал на платформе 8.3.27. В тело положил простую наценку Результат = Число(Вход) * 1.37;, ИНН покупателя и секрет, собрал блок КодПодИНН. Потом проверил его выполнение - не только счастливый путь:
- под верным ИНН тело выполняется: вход 1000 даёт
Результат1370, round-trip тела точный на длинах 0, 31, 32, 33, 64 байта и на многобайтном UTF-8 (кириллица, символы валют); - под чужим ИНН тег не сходится, тело не отдаётся и не выполняется, обработка бросает исключение;
- две упаковки одного тела одним ключом дают разные нонс и шифроблок - дыра повторной гаммы закрыта;
- подмена нонса или тега, пустой нонс - доступ закрыт.
В собранном блоке нонс ровно 32 символа, шифроблок и тег - одной строкой (переносы Base64Строка снимаются при сборке, иначе литерал в коде порвётся). Обработка не пишет в базу, не лезет в таблицы напрямую и работает только штатными методами платформы.
Честно про потолок: чего это не делает
А теперь то, из-за чего я переписал эту статью после внешнего ревью. Привязку к одному покупателю шифрование не гарантирует, и обещать обратное было бы враньём. Секрет разработчика по определению едет внутри клиентского модуля боевой обработки, и из этого следуют два обхода, которым даже не нужно доставать секрет.
- Подставить чужой ИНН. У кого есть доступ к редактированию модуля, тот подставляет в вызов расшифровки ИНН законного покупателя, с чьей копии разошёлся файл. Код сам знает секрет и сам всё расшифрует.
- Снять открытый текст после расшифровки. У законного покупателя
КодПодИННштатно расшифровывает тело в памяти - его оттуда и забирают, не трогая ни секрет, ни схему.
Поэтому никакого "взломать дороже, чем купить" я не обещаю: доказанного порога стоимости тут нет. Пароль на модуль, кстати, защищает и обычную проверку ИНН, так что сам по себе он преимущества шифрования не создаёт.
Что шифрование реально даёт, честно: тело процедуры не видно в открытом виде, и случайное распространение файла не раскрывает алгоритм и коэффициенты. Планка снятия привязки поднимается с "удалить три строки" до "возьми скопированный файл, узнай исходный ИНН покупателя и поправь вызов расшифровки". Рабочая база покупателя для этого не нужна, хватает файла. Насколько это кого-то остановит на практике - вопрос открытый, цифр у меня нет. Для защиты логики от прямого копирования это работает, для настоящей лицензии - нет.
Следующий уровень - подпись лицензии ключом, которого у покупателя в принципе нет, это уже асимметричная криптография. Как её поднять на чистом встроенном языке, без COM и .NET, я разбирал отдельно в статье RSA на встроенном языке 1С.
Демо-обработка
К статье приложена бесплатная демо-обработка: вводите тело процедуры, ИНН покупателя и секрет, жмёте "Собрать блок" - получаете готовую КодПодИНН для вставки, и тут же, ниже на форме, проверяете её выполнение под верным и под чужим ИНН. Наверху формы - описание, что она делает, а кнопка "Справка (FAQ)" открывает ответы на частые вопросы. Ядро внутри - ровно то, что выше по тексту, плюс полная реализация HMAC. Скачать можно на странице демо-обработки "Привязка процедуры к ИНН".
Другие наши инструменты:
- Анализ кода внешних обработок - разбирает чужую или свою
.epfи показывает, что внутри: опасные конструкции, прямой доступ к таблицам, тяжёлые места. Соседний угол к этой статье: там мы прячем процедуру, здесь читаем чужую. - Помощник перехода на 1С 8.5 - тот же разбор кода, только вопрос другой: что перестанет работать после смены платформы.
- Трансформатор SQL в 1С - переводит запрос из профайлера обратно в имена справочников и регистров, когда видно, что база тормозит, а чей это запрос непонятно.
- Чек-ап СУБД под 1С - диагностика сервера, когда код чистый, а тормозит всё равно.
Открытый вопрос, на который у меня нет однозначного ответа: стоит ли вообще городить клиентскую защиту для одной процедуры в интерпретируемом языке, где секрет всё равно едет к покупателю? Или честнее отдавать дёшево и держать привязку в договоре? Где у вас проходит граница между "защитил" и "потратил на защиту больше, чем она стоит"?
Вступайте в нашу телеграмм-группу Инфостарт
Вы можете заказать платную адаптацию этой статьи под ваши задачи на «Бирже заказов».
- 0% комиссии — оплата напрямую исполнителю;
- Исполнители любого масштаба — от отдельных специалистов до команд под проект;
- Прямой обмен контактами между заказчиком и исполнителем;
- Безопасная сделка — при необходимости;
- Рейтинги, кейсы и прозрачная система откликов.