Предлагаемая обработка является существенно переработанной версией другой обработки, опубликованной здесь ранее:
//infostart.ru/public/1223640/
Её автор Никита Хацкевич проделал большую работу по переводу обработки с языка C на язык 1С. Однако у неё, на мой взгляд, осталось две проблемы:
1. Формат входных и выходных данных: строки в HEX-формате. Удобнее было бы иметь дело с типом ДвоичныеДанные.
2. Крайне низкая производительность. Когда я попытался использовать модуль "как есть" - быстро понял, что это не получится, т. к. слишком долго. Пришлось засучить рукава и внести свой вклад )
Обе проблемы удалось полностью решить, но от исходной обработки мало что осталось.
Первым делом был изменён тип входных данных - обработка сразу показала неплохой прирост в производительности. Но наиболее существенный прирост был получен за счёт предварительного расчёта всего, что можно было рассчитать. Эти данные были помещены в строку JSON, из которой при запуске полностью формируется готовая структура Const. Производительность выросла на порядок.
Следующей проблемой оказалась функция платформы "ПобитовоеИсключительноеИли" - она оказалась в топе по накладным расходам. Ок - делаем двумерный массив с посчитанными значениями. Но применить тот же приём со строкой JSON не удалось, так как загрузка этой строки стала занимать существенную часть времени. Тогда был применён другой приём - весь массив был записан побайтно в файл и затем загружен в макет. При старте вычислений из макета получается переменная типа ДвоичныеДанные, которая нарезается в массив буферов по 256 байт. Это отрабатывает быстро, но прирост в производительности от замены функции платформы составил всего 1,5-2 раза. Если вам требуется просто модуль, а не обработка, без каких-либо макетов - то стоит заменить обратно "XOR" на вызов типовой функции "ПобитовоеИсключительноеИли".
Ещё некоторое количество миллисекунд удалось выудить за счёт сокращения вложенности вызовов функций. Кое где в ущерб читаемости.
Заметка о самом алгоритме
Шифр блочный и поэтому если входные данные не кратны 16 байтам - "хвостик" забивается нулями. Это не очень хорошо с точки зрения криптостойкости, поэтому лучше, чтобы это были не нули, а случайные значения. Но в этом случае становится непонятна длина исходных данных. Помещать длину в начало или в конец - тоже не очень хорошо. Лучше передавать её отдельно и затем обрезать результат расшифровки до нужной длины.
Данная проблема в обработке не была отработана, так как не всем это нужно.
Тесты
Обработка тестировалась на платформе 1С:Предприятие 8.3 (8.3.16.1502).
Процедура тестирования исходной обработки:
&НаСервере
Процедура ТестСкоростиНаСервере()
ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
СчётчикПаролей = 1;
ИсходнаяСтрока = "";
Для ЧислоБлоков = 1 по 32 Цикл
Данные = ПолучитьДанные(ЧислоБлоков);
ДанныеHEX = ПолучитьHexСтрокуИзДвоичныхДанных(Данные);
СреднееВремя = 0;
Для НомерИтерации = 1 по 10 Цикл
Ключ256 = ПолучитьКлюч256(Формат(СчётчикПаролей, ""));
Ключ256_HEX = ПолучитьHexСтрокуИзДвоичныхДанных(Ключ256);
СчётчикПаролей = СчётчикПаролей + 1;
Время1 = ТекущаяУниверсальнаяДатаВМиллисекундах(); // СТАРТ!
РезультатHEX = ОбработкаОбъект.Зашифровать(ДанныеHEX, Ключ256_HEX, 256);
ИсходныеДанные = ОбработкаОбъект.Расшифровать(РезультатHEX, Ключ256_HEX, 256);
Время2 = ТекущаяУниверсальнаяДатаВМиллисекундах(); // СТОП!
СреднееВремя = СреднееВремя + Время2 - Время1;
КонецЦикла;
ТекстСообщения = СтрШаблон(
"%1;%2"
,Формат(ЧислоБлоков, "ЧН=; ЧГ=")
,Формат(СреднееВремя / 10, "ЧРД=,; ЧН=; ЧГ=")
);
Сообщить(ТекстСообщения);
КонецЦикла;
КонецПроцедуры
Результаты. Первый столбец - число блоков в сообщении. Второй столбец - среднее количество миллисекунд, необходимое для зашифровки и расшифровки этого сообщения 256-битным ключом за 10 итераций. Ключ на каждой итерации новый.
Число блоков | млсек |
1 | 1172,7 |
2 | 2095,9 |
3 | 3337,3 |
4 | 4462,9 |
5 | 5461 |
6 | 6124,4 |
7 | 7587,5 |
8 | 9798,7 |
9 | 10776,4 |
10 | 12567,2 |
11 | 13681,9 |
12 | 14596,7 |
13 | 15450,6 |
14 | 16352 |
15 | 17452,1 |
16 | 18694,3 |
17 | 20562,8 |
Процедура тестирования этой обработки:
&НаСервере
Процедура ТестСкоростиНаСервере()
ОбработкаОбъект = РеквизитФормыВЗначение("Объект");
СчётчикПаролей = 1;
ИсходнаяСтрока = "";
Для ЧислоБлоков = 1 по 32 Цикл
Данные = ПолучитьДанные(ЧислоБлоков);
СреднееВремя = 0;
Для НомерИтерации = 1 по 10 Цикл
Ключ256 = ПолучитьКлюч256(Формат(СчётчикПаролей, ""));
СчётчикПаролей = СчётчикПаролей + 1;
Время1 = ТекущаяУниверсальнаяДатаВМиллисекундах(); // СТАРТ!
Результат = ОбработкаОбъект.Зашифровать(Данные, Ключ256, 256);
ИсходныеДанные = ОбработкаОбъект.Расшифровать(Результат, Ключ256, 256);
Время2 = ТекущаяУниверсальнаяДатаВМиллисекундах(); // СТОП!
СреднееВремя = СреднееВремя + Время2 - Время1;
КонецЦикла;
ТекстСообщения = СтрШаблон(
"%1;%2"
,Формат(ЧислоБлоков, "ЧН=; ЧГ=")
,Формат(СреднееВремя / 10, "ЧРД=,; ЧН=; ЧГ=")
);
Сообщить(ТекстСообщения);
КонецЦикла;
КонецПроцедуры
Результат. Столбцы те же.
Число блоков | млсек |
1 | 63,7 |
2 | 107,2 |
3 | 142,1 |
4 | 184,1 |
5 | 222,1 |
6 | 253,2 |
7 | 303,8 |
8 | 322,1 |
9 | 396,3 |
10 | 417,3 |
11 | 439,8 |
12 | 495,6 |
13 | 598,3 |
14 | 601,8 |
15 | 680,9 |
16 | 743,9 |
17 | 806,5 |
18 | 847,8 |
19 | 795,7 |
20 | 687 |
21 | 915,6 |
22 | 947,6 |
23 | 927,3 |
24 | 944,6 |
25 | 978,6 |
26 | 1019,1 |
27 | 1076,8 |
28 | 1095,7 |
29 | 1149,2 |
30 | 1251,6 |
31 | 1260,3 |
32 | 1221,4 |
17 блоков - 20562,8 млсек и 806,5. Итого: ускорение в 25,5 раз.
Оба результата в виде графика:
Важное дополнение (UPD)
Обработка в точности воспроизводит алгоритм. А форма содержит дополнительную операцию подготовки парольной фразы - мы получаем хэш пароля. Так учитывается весь пароль, какой бы длины он не был.
Кто-то так не делает и подаёт на вход пароль как есть. В этом случае, если он меньше 32 байт, то функция шифровки должна заполнить его нулями. Если она больше 32 байт, то функция шифровки возьмёт только первые 32 байта, остальная часть пароля не будет никак использоваться.
Кроме того, при получении данных при шифровке, должно производиться заполнение буфера до конца нулями. Так как длина буфера кратна размеру блока (16 байт). Кто-то так тоже не делает. Тогда в блок попадает мусор, который был в памяти. В этом случае при расшифровке надо быть готовым, что этот мусор попадёт в буфер результата.
Для примера разберём вот этот онлайн-шифровальщик: https://encode-decode.com/aes-256-ecb-encrypt-online/
В нём некорректно работает заполнение входных данных. Если ввести текст длиной 15 байт, то результат имеет размер один блок. Если ввести текст длиной 16 байт, то результат имеет длину два блока, хотя должен быть один. В качестве заполнителя используются не нули, а случайные значения. Это неверно, так как при расшифровке (на другом клиенте) они добавятся к результату.
Данный сайт подаёт в функцию сам пароль, а не его хэш. Чтобы в этом убедиться нужно подать пароль длинный пароль и его усечённую версию, но чтобы оба были длиннее 32 байт. Результат шифровки будет одинаковый. Например такие:
password 32 bytes long .........
password 32 bytes long .........1234567
Чтобы нам подать сам пароль, а не его хэш, нужно изменить функцию получения двоичных данных пароля (на форме). Она должна быть такой:
&НаСервере
Функция ПолучитьКлюч256(Пароль)
Поток = Новый ПотокВПамяти();
ЗаписьДанных = Новый ЗаписьДанных(Поток);
ДлинаТекста = СтрДлина(Пароль);
Если ДлинаТекста < 32 Тогда
ЗаписьДанных.ЗаписатьСимволы(Пароль);
Для НомерБайта = ДлинаТекста + 1 по 32 Цикл
ЗаписьДанных.ЗаписатьБайт(0);
КонецЦикла;
Иначе
ЗаписьДанных.ЗаписатьСимволы(Лев(Пароль, 32));
КонецЕсли;
ЗаписьДанных.Закрыть();
Возврат Поток.ЗакрытьИПолучитьДвоичныеДанные();
КонецФункции // ПолучитьКлюч
Попробуем шифровать на сайте и обработкой. Поскольку следующее случайное число мы не знаем (которое подаст данный конкретный сайт), то чтобы попробовать - подадим текст длинной в один блок (16 байт). А пароль подадим длиной 32 байта.
Входной текст = test string.....
Пароль = password 32 bytes long .........
Результат сайт = Z1kiP3YmXCMzifLooBZDOj364gptpei1FpyuX8beKOo= (base64)
или 67 59 22 3f 76 26 5c 23 33 89 f2 e8 a0 16 43 3a 3d fa e2 0a 6d a5 e8 b5 16 9c ae 5f c6 de 28 ea (HEX, два блока, во втором заполнитель, лишний)
Результат обработки = 67 59 22 3F 76 26 5C 23 33 89 F2 E8 A0 16 43 3A (один блок, как и должно быть).
Для идентичного результата надо использовать тот же заполнитель. При расшифровке данных с этого сайта, чтобы не получить ошибку платформы при передаче этих случайных значений с клиента на сервер, нужно изменить процедуру формы "РасшифроватьНаСервере", ноль заменить на значение заполнителя, например если он = 16, то так:
ПозицияХвоста = СтрНайти(ИсходныйТекстСХвостом, Символ(16)); // 0 -> 16
Вот пример скрипта на PHP, который работает точно так же, как и вся обработка, вместе с формой:
<?php
$string="test string";
// Функция "openssl_encrypt" - она по умолчанию не забивает блок нулями до конца. Поэтому делаем это сами.
// Помним, что нам нужна не строка, а бинарные данные.
if (strlen($string) % 16) {
$string = str_pad($string, strlen($string) + 16 - strlen($string) % 16, "\0");
}
$method="AES-256-ECB";
$pass="pass123";
// Что делает функция "hash"?
// Вот по ней документация: https://www.php.net/manual/ru/function.hash.php
// Она отдаёт бинарные данные, только если указан третий параметр как Истина, по умолчанию он - Ложь.
// "При false выводит данные в шестнадцатеричной кодировке в нижнем регистре." - то есть возвращает СТРОКУ.
// Поэтому третий параметр должен быть = Истина.
$keyBin = hash('sha256', $pass, true);
echo("encrypt KEY ".bin2hex($keyBin)."\n");
// Смотрим, что делает openssl_encrypt: https://www.php.net/manual/ru/function.openssl-encrypt.php
// Шифрует данные с заданным шифром и ключом и возвращает необработанную строку, либо строку кодированную в base64.
// Видимо необработанная строка при опции OPENSSL_RAW_DATA и base64 - в остальных случаях.
// Давайте получим двоичные данные.
$encrypt=openssl_encrypt($string, $method, $keyBin, OPENSSL_RAW_DATA | OPENSSL_NO_PADDING);
if (!$encrypt) {
echo("Что-то пошло не так...");
return;
}
// Выводим результат, преобразовав двоичные данные в HEX-строку:
$res = bin2hex($encrypt);
echo("encrypt HEX ".$res."\n");
?>
По поводу символов заполнителей - нигде не прописано, каким он должен быть. Нужна какая-то статистика, кто какое значение использует. Обычно это ноль. В этом случае, при конвертации в строку, всегда у всех отрабатывает правильно. Буду рад, если напишете в комментариях, какие варианты вы ещё встречали.
Версия 2.1
Учитывая всё вышеперечисленное, имеет смысл вынести заполнитель и способ подачи пароля в параметры. Что я и сделал в обновлённой версии. Используя данные параметры, можно добиться идентичных результатов с различными онлайн-шифровальщиками.
Спасибо за внимание.