вторник, 26 июня 2018 г.

О некоторых изменениях в криптографических алгоритмах ESCK-5 и CSA-2

Трунов Д.Н.
О некоторых изменениях в криптографических алгоритмах 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