Статья: Работа с бинарными данными и реестром Windows на платформе .NET

В статье описывается набор классов, которые могут использоваться для быстрой работы с бинарными данными, в том числе, для записи данных различного типа в поток, чтения из потока, сжатия, шифрования, контроля целостности данных, а также для облегчения работы с реестром Windows из приложений на платформе .NET. Исходный код библиотеки AcedUtils.NET и демонстрационное приложение прилагаются к статье.

Предисловие

Основной целью разработки AcedUtils.NET было стремление создать классы для эффективного выполнения основных операций с данными, включая сжатие, шифрование, работу с бинарным потоком. Весь код библиотеки написан на языке C# и максимально оптимизирован по быстродействию.

Библиотека AcedUtils.NET содержит следующие классы, принадлежащие пространству имен AcedUtils:

AcedBinary – содержит статические методы и функции для работы с бинарными данными, в том числе для вычисления контрольной суммы Адлера, для копирования массивов байт и работы с массивами чисел типа Int32.

AcedRipeMD – используется для вычисления значения односторонней хеш-функции RipeMD-160 массива байт или строки символов. Включает методы для копирования и сравнения цифровой сигнатуры, преобразования ее в значение типа Guid, очистки массива, содержащего цифровую сигнатуру.

AcedCast5 – предназначен для шифрования и дешифрования массива байт методом CAST-128 в режиме CFB (64 бита). Блочный алгоритм шифрования реализован в соответствии с RFC 2144. Алгоритм отличается высоким быстродействием и надежностью.

AcedDeflator, AcedInflator – используются для сжатия и распаковки массива байт с помощью алгоритма LZ+Huffman.

AcedMemoryReader, AcedMemoryWriter – предназначены для помещения данных в бинарный поток и чтения из потока. При использовании этих классов бинарный поток представляется массивом типа byte[], размер которого динамически увеличивается по мере добавления новых данных. При этом сами данные могут быть упакованы с применением оригинального алгоритма сжатия, зашифрованы методом CAST-128 и защищены значением цифровой сигнатуры RipeMD-160.

AcedStreamReader, AcedStreamWriter – предназначены для помещения данных в бинарный поток и чтения данных из потока. Здесь, в отличие от классов AcedMemoryReader и AcedMemoryWriter, размер бинарного потока не ограничивается объемом оперативной памяти. Данные помещаются в поток и читаются из потока типа System.IO.Stream, который ассоциируется, соответственно, с экземпляром класса AcedStreamWriter или AcedStreamReader.

AcedReaderStream, AcedWriterStream – классы-оболочки, позволяющие работать с перечисленными выше классами бинарных потоков так, как будто они являются потомками класса System.IO.Stream.

AcedRegistry – объединяет методы для сохранения в реестре Windows значений различного типа, в том числе, строк, массивов байт, значений типа Boolean, DateTime, Decimal, Double и т.д. Кроме того, в AcedRegistry находятся методы для чтения соответствующих значений из реестра Windows.

Рассмотрим подробнее каждый из перечисленных классов.

Класс AcedBinary

В AcedBinary собраны функции для работы с бинарными данными, которые используются другими классами в составе AcedUtils.NET. Однако, они могут вызываться и из прикладной программы. Например, функция SwapBytes() обращает порядок следования байт в значении типа System.UInt32, функция ReverseBits() выполняет аналогичное действие с битами в составе двойного слова. Точнее, размер исходного значения может варьироваться от 1 до 32 бит. Функция Adler32() вычисляет значение контрольной суммы Адлера в соответствии с RFC 1950 для массива байт или его фрагмента. Данный алгоритм расчета контрольной суммы отличается от CRC32 большей производительностью. В этом классе есть еще несколько unsafe-методов, предназначенных для копирования массива байт, быстрого заполнения массива чисел типа System.Int32 и копирования одного такого массива в другой.

Класс AcedRipeMD

Смысл односторонней хеш-функции заключается в том, что практически невозможно подобрать другой набор байт, для которого значение цифровой сигнатуры совпадало бы с исходным значением. Кроме того, невозможно восстановить состояние исходных данных по известному значению цифровой сигнатуры. Класс AcedRipeMD реализует алгоритм расчета односторонней хеш-функции RipeMD-160 в полном соответствии с документом: "RIPEMD-160: A Strengthened Version of RIPEMD" (Hans Dobbertin, Antoon Bosselaers, Bart Preneel), April 18, 1996. Длина получаемой сигнатуры составляет 20 байт (160 бит). Цифровую сигнатуру удобно представить в виде массива из 5 элементов типа System.Int32.

Чтобы получить значение односторонней хеш-функции для массива байт или строки символов, можно воспользоваться функцией Compute() класса AcedRipeMD. При передаче в нее массива байт указывается смещение и длина обрабатываемого фрагмента массива. Имеется также unsafe-вариант этой функции, принимающий в качестве параметра указатель на массив байт. Иногда, например, при работе с потоком данных, требуется рассчитать цифровую сигнатуру для массива байт, представленного в виде нескольких фрагментов. В этом случае можно применить функции для поточного расчета сигнатуры RipeMD-160. Для этого сначала вызывается функция Initialize, которая возвращает или заполняет служебный массив hashData. Затем нужно последовательно вызвать метод Update для каждого фрагмента данных. В этот метод передается массив hashData, а также ссылка на первый или следующий фрагмент данных в виде массива байт или строки символов, для которого вычисляется значение сигнатуры. После того, как функция Update была вызвана для каждого фрагмента, можно получить само значение цифровой сигнатуры вызовом метода Finalize().

Алгоритм шифрования CAST-128, используемый при работе с бинарным потоком классами Aced(…)Writer/Aced(…)Reader, предполагает, что длина ключа шифра составляет 128 бит. Цифровая сигнатура RipeMD-160 как нельзя лучше подходит для использования ее в качестве ключа при шифровании данных. Однако, она представляется числом размером 160 бит, а не 128. Для решения этой проблемы в класс AcedRipeMD добавлена функция ToGuid(). Она принимает значение 20-байтной цифровой сигнатуры и возвращает соответствующее ему значение типа System.Guid, размер которого составляет 128 бит.

В классе AcedRipeMD есть еще несколько вспомогательных методов, облегчающих работу с цифровой сигнатурой, представленной в виде массива из 5 значений типа System.Int32. Например, функция Copy() позволяет быстро скопировать значение хеш-функции в массив байт или, наоборот, считать его из массива байт. Функция Equals() используется для проверки равенства двух значений цифровой сигнатуры, одно из которых может быть представлено массивом байт. Функция Clear() обнуляет 5 элементов массива типа System.Int32[], предназначенного для хранения сигнатуры RipeMD-160.

Класс AcedCast5

В AcedCast5 реализован алгоритм CAST-128 (CAST5) в соответствии с RFC 2144. Это незапатентованный алгоритм шифрования с ключом размером 128 бит, отличающийся высоким быстродействием и стойкостью к различным видам криптоанализа. При применении шифра к данным используется режим обратной загрузки шифротекста (CFB) с размером блока входных данных 64 бита. Класс AcedCast5 используется при шифровании и дешифровании бинарного потока данных, представленного классами Aced(…)Writer/Aced(…)Reader. Кроме того, он может применяться самостоятельно для шифрования произвольных данных.

Два основных метода класса AcedCast5, методы Encrypt() и Decrypt(), предназначены, соответственно, для шифрования и дешифрования массива байт или его фрагмента с ключом, который задается параметром keyBytes в виде 16-байтного массива. Если в программе ключ представляется значением типа System.Guid, то соответствующий ему массив байт можно получить вызовом функции Guid.ToByteArray(). Одновременно с шифрованием в классе AcedCast5 вычисляется значение односторонней хеш-функции RipeMD-160 для шифруемых данных. Функция Encrypt() возвращает массив из 5 значений типа System.Int32, представляющих собой цифровую сигнатуру фрагмента данных, рассчитанную до того, как данные были зашифрованы. Функция Decrypt() возвращает аналогичный массив, представляющий цифровую сигнатуру фрагмента данных, рассчитанную после того, как данные были расшифрованы. Если при шифровании и дешифровании использован один и тот же ключ и данные в массиве не были повреждены, функции Encrypt() и Decrypt() должны вернуть одно и тоже значение хеш-функции RipeMD-160. Имеются также unsafe-варианты этих функций, в которые передается указатель на массив шифруемых байт. Кроме того, функции Encrypt() и Decrypt() могут принимать параметр iv, задающий начальный вектор для шифрования или дешифрования данных.

В классе AcedCast5 есть функций для шифрования данных, представленных несколькими фрагментами, т.е. поточного шифрования. В частности, функция ScheduleKey() на основе ключа шифра keyBytes создает или заполняет специальный массив key, содержащий ключевую информацию, который передается затем в качестве ключа в остальные функции, относящиеся к данному разделу. Таким образом, ключевой массив создается только однажды, а не перед каждым шифрованием следующего фрагмента данных. Функция GetOrdinaryIV() возвращает значение, которое может использоваться в качестве начального вектора. Это значение получается шифрованием нулевого вектора с помощью текущего ключа шифра. Функции Encrypt() и Decrypt(), которые принимают параметр key, используются непосредственного для шифрования и дешифрования данных в поточном режиме. Каждая из этих функций, кроме ключа и ссылки на шифруемые или дешифруемые данные, принимает параметр iv, в котором передается значение начального вектора. Новое значение вектора, которое может использоваться при следующем вызове функций Encrypt()/Decrypt(), возвращается как результат функции. Когда все данные зашифрованы или расшифрованы, нужно вызвать метод ClearKey() для очистки массива key, содержащего ключевую информацию. А можно вместо или после этого передать массив key в метод ScheduleKey() для заполнения его информацией о новом ключе для шифрования других данных с другим ключом без пересоздания массива key.

Классы AcedDeflator и AcedInflator

Эти классы предназначены для сжатия и распаковки данных, представленных массивом байт или его фрагментом. Применяемый алгоритм сжатия аналогичен описанному в RFC 1951 и реализованному в библиотеке zlib, но имеет ряд отличий, в частности, использует другой формат блока данных. Формат описан в исходном коде библиотеки в начале файла Compressor.cs.

Упаковка данных производится методом Compress() класса AcedDeflator, распаковка – методом Decompress() класса AcedInflator. Особенность работы с этими классами заключается в том, что их экземпляры не следует создавать напрямую вызовом конструктора. Лучше вместо этого использовать ссылку на единственный экземпляр каждого класса, которую можно получить, обратившись к статическому свойству Instance. Это ограничение связано с тем, что при создании экземпляров этих классов, особенно класса AcedDeflator, выделяется значительный объем памяти под внутренние массивы. Обычно не требуется использовать параллельно несколько экземпляров архиватора. Кроме того, частое перераспределение памяти ведет к снижению производительности. При первом обращении к свойству Instance создается один экземпляр соответствующего класса. Ссылка на него сохраняется в статическом поле класса и возвращается при каждом следующем обращении к свойству Instance. Когда возникает необходимость освобождения памяти, занятой внутренними массивами архиватора, можно вызвать статический метод Release для обнуления внутренней ссылки на экземпляр соответствующего класса. Тогда, если нет других ссылок на этот экземпляр, при следующей "сборке мусора" память будет возвращена операционной системе.

Для сжатия данных функцией AcedDeflator.Compress() в нее передается ссылка на массив байт с указанием смещения и длины сжимаемого фрагмента данных. Есть два варианта этой функции. В первом случае память под массив для сохранения упакованных данных распределяется самой функцией Compress(). Параметры beforeGap и afterGap этой функции задают отступ, соответственно, в начале и в конце выходного массива на случай, если кроме упакованных данных в него должна быть помещена еще какая-то информация. Во втором случае в функцию Compress() передается ссылка на уже существующий массив, в который должны быть записаны упакованные данные, а также смещение в этом массиве, с которого начинается запись. Максимальный размер упакованного фрагмента в случае, если данные несжимаемы, равен длине исходного фрагмента плюс 4 байта. Таким образом, длина приемного массива должна быть достаточна для хранения исходного фрагмента данных плюс 4 байта и плюс смещение в этом массиве. Функция Compress() возвращает размер сжатого фрагмента, т.е. число байт, сохраненное в выходном массиве.

Параметр типа AcedCompressionMode, передаваемый в функции Compress(), выбирает режим сжатия данных. Он принимает одно из следующих значений: NoCompression – данные не сжимаются, а просто копируются в входной массив с добавлением 4-байтной длины фрагмента для последующей его распаковки; Fastest – самый быстрый режим сжатия, который, тем не менее, может быть эффективен для некоторых типов данных; Fast – используется режим быстрого сжатия, когда максимальное расстояние между повторяющимися последовательностями во входном потоке принимается равным 65535 байтам; Normal – обычное сжатие, когда максимальное расстояние между последовательностями составляет 131071 байт; MaximumCompression – максимальное сжатие, доступное данному архиватору, предполагающее, что максимальное расстояние между повторяющимися последовательностями составляет 262143 байта.

Сжатые данные распаковываются методом AcedInflator.Decompress(). Прежде чем вызывать этот метод необходимо подготовить область памяти, достаточную для хранения результата. Узнать первоначальный размер сжатых данных можно вызовом статической функции GetDecompressedLength() класса AcedInflator. В нее передается ссылка на массив байт и смещение в этом массиве, с которого начинаются упакованные данные. Функция возвращает длину фрагмента данных после его распаковки. Затем можно создать массив байт достаточного размера и передать его в функцию Decompress() для заполнения распакованными данными. Эта функция принимает ссылку на исходный массив, содержащий сжатые данные, смещение в этом массиве, а также ссылку на приемный массив, в который выполняется распаковка, и смещение в приемном массиве. Функция возвращает число байт сохраненное в выходном массиве. Есть еще другой вариант функции Decompress(), в котором память под выходной массив распределяется самой функцией. Эта функция принимает параметры beforeGap и afterGap, которые задают число байт, которое надо зарезервировать, соответственно, в начале и в конце выходного массива.

--> ЧИТАТЬ ПОЛНОСТЬЮ <--

К-во Просмотров: 264
Бесплатно скачать Статья: Работа с бинарными данными и реестром Windows на платформе .NET