Трунов
Д.Н.
О некоторых изменениях в криптографических алгоритмах ESCK-5 и CSA-2
Введение
В связи с переводом на язык
программирования C#
алгоритмов шифрования ESCK-5
[1] и криптографичекой подписи CSA-2
[2], изначально написанных в Delphi
(Object
Pascal),
были обнаружены отдельные особенности этих алгоритмов, не позволяющие
реализовать их одинаково корректно и на C#, и в Delphi. Основное отличие заключается в
том, что изначальные версии алгоритмов оперировали с текстовыми строками
преимущественно в формате (кодировке) Ansi, в то время, как язык C# ориентирован на работу со
строками в формате Юникод (Unicode).
Во избежание путаницы и других проблем следовало выбрать только один подход,
позволяющий корректно реализовать его и на том, и на другом языке программирования.
Формат текста в данных алгоритмах имеет
принципиальное значение. Пароль для алгоритма ESCK-5 в виде байтового массива влияет
на генерацию ключа шифрования. Один и тот же текстовый пароль, преобразованный
в байтовый массив с применением двух разных кодировок, может приводить к
созданию двух разных ключей шифрования. Строка результата вычисления CSA-2 может быть задействована в
следующем вычислении, что тоже может дать уже два разных результата в
зависимости от выбранной кодировки этой строки.
Ansi
или Unicode?
Название Ansi для
кодировки текста является несколько некорректным, но прижившимся. По сути, Ansi –
это вариант расширенной до 8 бит 7-битной таблицы символов US-ASCII [3]. Как и в случае других
8-битных кодировок, для Ansi
характерна
проблема ограниченности набора символов, потому для распространённых алфавитов существуют
отдельные таблицы символов, называемые кодовыми страницами [4]. Тем не менее,
применение кодовых страниц всё равно накладывает свои ограничения, например, если
нужно работать с текстом, включающим символы одновременно из нескольких разных алфавитов.
Кроме того, могут возникнуть проблемы отображения на компьютере с одной
активной кодовой страницей текста, созданного на компьютере с другой кодовой
страницей.
Юникод является «широкой» кодировкой,
включающей в себя знаки почти всех письменных языков мира, при этом в нём
становится ненужным переключение кодовых страниц [5]. Большинство современных
операционных систем и языков программирования в той или иной степени поддерживают
Юникод, а в некоторых (например, среде программирования Java) принципиально не поддерживаются
8-битные кодировки. Юникод в настоящее время является доминирующим в интернете
и продолжает вытеснять другие форматы за счёт своего удобства и
универсальности. Потому будет логичным и в алгоритмах ESCK-5 и CSA-2 также остановиться на Юникоде.
Отказ
от Ansi-реализаций
В связи с заявленными изменениями, любые
текстовые строки в процедурах и функциях алгоритмов ESCK-5 и CSA-2 должны быть в формате Юникод.
Однако в алгоритме CSA-2
ранее были реализованы две функции для вычисления криптографической подписи
строки: SignStringA
(для AnsiString)
и SignStringW
(для WideString).
В изменённой версии алгоритма должна остаться одна функция SignString, принимающая строку S и
возвращающая результат в формате Юникод:
// объявление функции в Delphi
Function SignString(S: String; Cycles: Cardinal):
String;
// объявление функции в C#
public string
SignString(string S, uint Cycles)
Следует учесть, что в новых версиях Delphi (начиная с 2009 года) стандартный
тип String
является
эквивалентным типу UnicodeString
[6,
7], поэтому его допускается использовать по умолчанию. В других реализациях Object Pascal (Lazarus, Free Pascal) это может быть по-другому,
поэтому вместо String
может
понадобиться указывать конкретный тип. Тогда объявление процедур и функций
может выглядеть, как показано на примере процедуры Gen из
ESCK-5:
// в Delphi с 2009, когда String = UnicodeString
Procedure Gen(Psw:
String; Cycles: Cardinal; Mode: Byte);
// в других случаях, когда String <> UnicodeString
Procedure Gen(Psw:
UnicodeString; Cycles: Cardinal; Mode: Byte);
// в случае, если UnicodeString вообще не поддерживается
Procedure Gen(Psw: WideString;
Cycles: Cardinal; Mode: Byte);
Различия между типами WideString и
UnicodeString
не
слишком большие. Оба вида строк оперируют с Юникодом в системе UTF16, когда каждый символ кодируется
преимущественно двумя байтами (16 бит). Но в некоторых случаях символы могут
занимать и больше двух байт, формируя так называемые «суррогатные пары».
Основное отличие заключается в том, что в WideString каждая такая пара
рассматривается как два отдельных символа, в то время, как UnicodeString может оперировать ею
как одним символом. Обычно это играет существенную роль только в случаях,
например, когда нужно обратиться к i-му
символу строки или извлечь подстроку из строки: возможен неверный учёт индекса
или разрыв строки на середине 4-байтного символа.
В языке C# всё намного проще: есть строковый
тип String,
который и подразумевает работу со строками в формате Юникод. Та же функция Gen в
C#
объявляется следующим единственным образом:
public void Gen(string
Psw, uint Cycles, byte Mode)
Загрузка
пароля
В связи с изменениями, требуется иной
подход и к загрузке пароля для генерации ключа шифрования. Изначально для этого
использовалась функция Ord,
возвращающая для каждого символа пароля его числовое значение, которое и добавлялось
к массиву ключа K.
Ниже приведён фрагмент первоначального варианта функции Gen с загрузкой пароля (актуально и
для ESCK-5,
и для CSA-2):
//
получение длины пароля
L :=
Length(Passwd);
//
усечение длины на случай превышения 256 символов
If L >
256 then
L :=
256;
//
посимвольная загрузка пароля и первичная обработка ключа
For J := 1 to 10 do
Begin
For I := 1 to L do
K[I - 1] := K[I - 1] + Ord(Passwd[I]);
GFunc;
End;
Эта реализация неудачна тем, что в
старых версиях Delphi
функция Ord
возвращает
числовое значение символа в пределах одного байта, хотя символ Юникода занимает
минимум 2 байта. Никто не заметит разницу, если пароль будет состоять только из
символов английского алфавита, однако использование других алфавитов Юникода
может проявить себя непредсказуемо в разных версиях Object Pascal. Ещё более непредсказуемым будет
поведение такого алгоритма при появлении суррогатных пар – символов, занимающих
не 2, а 4 байта.
Более универсальный подход предполагает
преобразование Юникод-строки в байтовый массив с помощью стандартных средств, а
затем добавление элементов этого массива к массиву ключа: по 2 байта из
байтового массива на каждую ячейку ключа K. В C# это можно реализовать с помощью Encoding.Getbytes:
// в секции using должно быть прописано:
// using System.Text;
// преобразование пароля в байтовый массив
byte[] P =
Encoding.Unicode.GetBytes(Passw);
// получение длины массива (количество байтовых пар)
int
L
= P.Length / 2;
// усечение длины на случай превышения 256 пар
if (L > 256)
L = 256;
// попарная загрузка байтов пароля в ключ и
первичная обработка ключа
for (J = 1; J <= 10;
J++)
{
for (I = 0; I < L; I++)
{
uint C = P[I * 2 + 1];
C = (C << 8) + P[I * 2];
K[I] = K[I] + C;
}
GFunc();
}
В Delphi для
этих целей можно использовать функцию WideCharToMultiByte из модуля Windows. Конечно, при таком
подходе 4-байтный символ, попадись он в пароле, будет разделён на две ячейки
ключа как два разных символа. Зато это исключает возможную путаницу, когда в
одной реализации все четыре байта будут записаны в одну ячейку, а в другой – по
два байта в две разные.
Криптографическая
подпись строки
При вычислении криптографической подписи
функцией SignString
требовалось изменить сразу два подхода. Во-первых, как и в случае с загрузкой
пароля, строку S
нужно
загружать в байтовый массив с помощью специальной функции (Encoding.GetBytes в
C#).
А, во-вторых, байтовый массив для этих целей нужно создавать динамически с
размером строго под загрузку строки S
(ранее для этих целей применялся статический массив фиксированного размера).
Ниже приведён текст функции SignString
на
языке C#:
public string
SignString(string S, uint Cycles)
{
byte[]
BM; // байтовый массив для вычисления
int
I; // вспомогательное целое
int
L; // длина строки
//
преобразование строки в байтовый массив
BM = Encoding.Unicode.GetBytes(S);
//
определение длины массива
L =
BM.Length;
//
увеличение длины до кратной 1024, если нужно
I = L;
if ((I % 1024) != 0)
{
I = ((I / 1024) + 1) * 1024;
Array.Resize(ref BM, I);
}
// вычисление подписи для массива
return SignMass(ref BM, (uint)(L), Cycles);
}
Объектный
класс CSA2Hash и производные классы
При переводе на язык C# не только алгоритм ESCK-5, но и CSA-2 был реализован в виде объектного
класса, в данном случае CSA2Hash, что позволяет использовать все
преимущества объектно-ориентированного программирования на C#: одновременная работа с
несколькими экземплярами класса, создание производных классов, перегрузка и
переопределение методов и т.д. [8]. Подобная реализация является более гибкой и
упрощает решение некоторых задач.
Так, к примеру, для одновременного
вычисления нескольких криптографических подписей одного и того же файла
необходимо создать нужные экземпляры класса CSAHash, для каждого
сгенерировать ключ шифрования с помощью функции Gen, затем загружать файл по частям в
специальный массив и обрабатывать этот массив с помощью функции EncriptMass. Проблема лишь в том,
что функции Gen
и
EncryptMass
недоступны
за пределами класса, а доступная функция SignFile подписывает
весь файл целиком и пока не закончит вычисление, не отдаст этот файл для работы
другим экземплярам класса. Если же сделать эти функции «видимыми» за пределами
класса с помощью модификатора доступа public, то это нарушит
принципы инкапсуляции и увеличит вероятность неправильного использования этих
функций.
Возможное решение заключается в том,
чтобы функции Gen
и
EncryptMass
сделать не полностью закрытыми (private),
а защищёнными (protected)
– невидимыми извне, но доступными производным классам. Тогда, при
необходимости, можно создать производный от CSA2Hash класс
и прописать в нём функции для работы с защищёнными методами. В качестве примера
ниже приведён производный класс CSA2New, в котором определены видимыми (public) новые функции NGen и
NEncryptFile.
Задача этих функций – лишь передать параметры и управление соответствующим
защищённым функциям базового класса:
public
class CSA2New : CSA2Hash
{
// Видимый
метод генерации ключа
public void NGen(uint Cycles)
{
base.Gen(Cycles);
}
// Видимый
метод шифрования массива
public void NEncryptMass(ref byte[] Mass, uint Size)
{
base.EncryptMass(ref Mass, Size);
}
}
Работа
с Ansi-строками
Несмотря на отказ от непосредственной
работы с текстовыми строками в кодировке Ansi, такая работа не исключается
полностью. Для корректной работы с Ansi-строкой её нужно лишь представить в виде байтового
массива. Если нужна криптографическая подпись строки, следует её вычислить для
этого массива с помощью функции SignMass.
Если же строка содержала пароль шифрования, то данные из полученного массива
нужно добавлять к элементам ключа K не по два, а по одному
байту. Для реализации такой возможности, как и в примере выше, можно создать
производный от CSAHash
класс и добавить в него нужные функции.
Поскольку C# не поддерживает Ansi-строки, байтовый массив нужно либо
загрузить извне (например, из файла), либо преобразовать из обычной (юникодной)
строки. Для этого можно использовать функцию Encoding.GetBytes, как в примере ниже:
// string
S – строка, преобразуемая в Ansi-формат
byte[] BM =
Encoding.Default.GetBytes(S);
В Delphi или
Lazarus
для
этих целей можно использовать тип AnsiString. В новых версиях Delphi преобразования
из String
(он же UnicodeString)
в AnsiString
происходит корректно. В Lazarus
есть
функции преобразования в модуле LazUTF8.
В старых версиях Delphi
нужно
выполнять преобразования между WideString
и
AnsiString
с
помощью отдельно написанных функций на основе WideCharToMultiByte и MultiByteToWideChar. Реализация таких
функций приведена, например, в [9].
Заключение
Описанные в статье изменения в
алгоритмах ESCK-5
и CSA-2
улучшают их работу и дают определённые преимущества:
1) принимается единственный
поддерживаемый формат текста – Юникод;
2) отпадает необходимость преобразований
формата текста, что исключает путаницу в этих форматах и преобразованиях;
3) принимается оптимальный для
реализации на разных языках программирования способ работы с широкими
(4-байтными) символами Юникода, снижается вероятность ошибочных реализаций;
4) улучшается функция вычисления
криптографической подписи строки, снимаются ограничения по максимальной длине
такой строки;
5) реализация алгоритмов в виде
объектных классов делает их более гибкими за счёт применения механизмов
наследования, переопределения методов или добавления новых.
Кроме того, переписанные на C# эти алгоритмы получают ещё одно
дополнительное преимущество: из-за сходств C# с языками программирования C++ и Java становится проще (при
необходимости) переписать алгоритмы и на эти языки.
Источники
информации
1. Трунов Д.Н. Алгоритм шифрования ESCK-5: описание, принцип работы,
исходные тексты [Электронный ресурс] – URL: http://d-raft5.blogspot.com/2018/04/esck-5.html
2. Трунов Д.Н. Описание и исходный текст
алгоритма криптографической подписи CSA-2 [Электронный ресурс] – URL: http://d-raft5.blogspot.com/2018/04/csa-2.html
3. Википедия. ASCII [Электронный
ресурс] – URL:
https://ru.wikipedia.org/wiki/ASCII
4. Википедия. Кодовая страница [Электронный
ресурс] – URL:
URL:
https://ru.wikipedia.org/wiki/Кодовая_страница
5. Википедия. Юникод [Электронный ресурс]
– URL:
https://ru.wikipedia.org/wiki/Юникод
6. Delphi в мире Юникода, часть I: что такое Юникод, зачем он Вам
нужен и как с ним работать в Delphi
[Электронный
ресурс] – URL:
http://edn.embarcadero.com/article/38446
7.String Types (Delphi) [Электронный ресурс] – URL: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/String_Types_(Delphi)
8. Шилдт, Герберт. Полный справочник по C#. Пер. с англ. – М.: Издательский
дом «Вильямс», 2004. – 752 с.: ил.
9. Delphi Sources. Как конвертировать WideString в
String
[Электронный
ресурс] – URL:
http://www.delphisources.ru/pages/faq/base/widestr_to_str.html
Комментариев нет:
Отправить комментарий