ГОСТ 28147-89 на C#
В одной из предыдущих статей я рассказывал как реализовать на C# алгоритм AES-256 американского стандарта шифрования данных AES. Теперь же я хочу рассказать про наш отечественный стандарт шифрования данных ГОСТ 28147-89 и о том, как реализовать его на C#. 
В стандарте предлагается один алгоритм шифрования и 4 режима его работы: режим простой замены, гаммирования, гаммирования с обратной связью и режим выработки имитовставки. Я коснусь только двух из них – режима простой замены и режима выработки имитовставки, так как остальные режимы основываются на шифровании в режиме простой замены.
Перейдем к описанию базового алгоритма шифрования. В отличие от предыдущей статьи, я не буду описывать его превдокодом, а сразу буду приводить листинги на C#. Итак, базовый алгоритм состоит из 32 тактов (раундов) и шифрует блок открытого текста длиной 64 бита на ключе длиной 256 бит. Раундовые подключи получаются из основного простым делением на 8 частей по 32 бита каждая, то есть основной ключ 256 бит равен (K0, ..., K7).
static void GostTransform(ref uint inBlockH, ref uint inBlockL, byte[,] pi, uint k) { uint outBlockH = Pi(pi, inBlockH + k); outBlockH = (outBlockH << 11) | (outBlockH >> 21); outBlockH ^= inBlockL; uint outBlockL = inBlockH; //копируем все на выход inBlockL = outBlockL; inBlockH = outBlockH; } static uint Pi(byte[,] sBox, uint inBlock) { if (sBox == null) throw new ArgumentNullException("sBox"); uint res = 0; res ^= sBox[0,(inBlock & 0x0000000f)]; res ^= (uint)(sBox[1, ((inBlock & 0x000000f0) >> 4)] << 4); res ^= (uint)(sBox[2, ((inBlock & 0x00000f00) >> 8)] << 8); res ^= (uint)(sBox[3, ((inBlock & 0x0000f000) >> 12)] << 12); res ^= (uint)(sBox[4, ((inBlock & 0x000f0000) >> 16)] << 16); res ^= (uint)(sBox[5, ((inBlock & 0x00f00000) >> 20)] << 20); res ^= (uint)(sBox[6, ((inBlock & 0x0f000000) >> 24)] << 24); res ^= (uint)(sBox[7, ((inBlock & 0xf0000000) >> 28)] << 28); return res; }
На вход каждого раунда подается два двойных слова inBlockH и inBlockL. Сначала к inBlockH прибавляется по модулю 2^32 раундовый ключ, затем результат делится на блоки по 4 бита и к каждому кусочку применяется одна из восьми таблиц замены (Pi - функция применения таблиц замены), далее результат циклически сдвигается на 11 разрядов влево, затем к полученному XORится inBlockL и помещается в переменную inBlockH. Предыдущее значение переменной inBlockH копируется в переменную inBlockL. По сути, просто осуществляем преобразования в соответствии с рисунком.

Функция зашифрования одного блока сообщения имеет вид:
private static void EncryptBlock(ref uint inH, ref uint inL, ref uint outH, ref uint outL, byte[,] pi, byte[] K) { uint m1 = inH; uint m2 = inL; GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 0)); //K0 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 4)); //K1 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 8)); //K2 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 12)); //K3 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 16)); //K4 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 20)); //K5 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 24)); //K6 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 28)); //K7 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 0)); //K0 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 4)); //K1 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 8)); //K2 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 12)); //K3 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 16)); //K4 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 20)); //K5 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 24)); //K6 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 28)); //K7 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 0)); //K0 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 4)); //K1 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 8)); //K2 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 12)); //K3 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 16)); //K4 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 20)); //K5 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 24)); //K6 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 28)); //K7 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 28)); //K7 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 24)); //K6 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 20)); //K5 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 16)); //K4 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 12)); //K3 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 8)); //K2 GostTransform(ref m1, ref m2, pi, Bytes2Dword(K, 4)); //K1 outH = m1; outL = Pi(pi, m1 + Bytes2Dword(K, 0)); outL = (outL << 11) | (outL >> 21); outL ^= m2; } private static uint Bytes2Dword(byte[] input, int offset) { if (offset + 4 > input.Length) throw new ArgumentOutOfRangeException(); return (uint)((input[offset+3] << 24) ^ (input[offset + 2] << 16) ^ (input[offset + 1] << 8) ^ (input[offset])); }
Как видно из листинга, на каждом в каждом раунде последовательно применяется 32 бита ключа шифрования, на раундах с 24 по 32 ключ шифрования применяется в обратном порядке.
Функция зашифрования сообщения в режиме простой замены (ECB) выглядит следующим образом:
public static void Gost28147Ecb(byte[] ot, byte[] st, byte[] k) { if (ot.Length != st.Length) throw new ArgumentException("Invalid input arrays length"); if ((ot.Length % 8) != 0) throw new ArgumentException("Invalid input arrays length"); int offset = 0; while(offset<ot.Length) { uint tempOutH = 0; uint tempOutL = 0; uint tempInH = Bytes2Dword(ot, offset); uint tempInL = Bytes2Dword(ot, offset + 4); EncryptBlock(ref tempInH, ref tempInL, ref tempOutH, ref tempOutL, p, k); Array.Copy(Dword2Bytes(tempOutH), 0, st, offset, 4); Array.Copy(Dword2Bytes(tempOutL), 0, st, offset + 4, 4); offset += 8; } }
Сначала проверяем длину открытого текста на кратность 8 (то есть она должна быть кратна 64 битам), так как дополнение блока до полного не наша забота
Процесс шифрования сводится к последовательному перемещению по массиву и пременению к каждым 8ми байтам нашей функции EncryptBlock. Режим простой замены готов. Запрограммируем режим выработки имитовставки. Функция выработки имитовставки для одного блока (8 байт) состоит из 16 раундов алгоритма ГОСТ. Выглядит она следующим образом:
private static void EncryptBlock16(ref uint inH, ref uint inL, ref uint outH, ref uint outL, byte[,] pi, byte[] k) { uint m1 = inH; uint m2 = inL; GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 0)); //K0 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 4)); //K1 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 8)); //K2 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 12)); //K3 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 16)); //K4 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 20)); //K5 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 24)); //K6 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 28)); //K7 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 0)); //K0 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 4)); //K1 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 8)); //K2 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 12)); //K3 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 16)); //K4 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 20)); //K5 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 24)); //K6 GostTransform(ref m1, ref m2, pi, Bytes2Dword(k, 28)); //K7 outH = m1; outL = m2; }
Как видишь, используется уже написанная функция GostTransform, представляющая собой один раунд алгоритма. Раундовые ключи применяются в прямом порядке два раза: K0...K7, K0...K7. Сама функция вычисления имитовставки для сообщения выглядит следующим образом:
public static void Gost28147Imito(byte[] ot, byte[] imito, byte[] k) { if (ot == null || imito == null || k == null) throw new ArgumentNullException(); if (imito.Length < 4 || k.Length < 32) throw new ArgumentException(); uint iterationsNumber = (uint) (ot.Length / 8.0); uint uImito = 0; for (uint i = 0; i < iterationsNumber; i++) { uint tempInH = Bytes2Dword(ot, (int) (i * 8)); uint tempInL = Bytes2Dword(ot, (int) (i * 8 + 4)); uint tempOutH = 0; uint tempOutL = 0; EncryptBlock16(ref tempInH, ref tempInL, ref tempOutH, ref tempOutL, p, k); uImito ^= tempOutH; } if (ot.Length % 8 != 0) { byte[] tempOt = new byte[8]; Array.Copy(ot, ot.Length - (ot.Length % 8), tempOt, 0, ot.Length % 8); uint tempInH = Bytes2Dword(tempOt, 0); uint tempInL = Bytes2Dword(tempOt, 4); uint tempOutH = 0; uint tempOutL = 0; EncryptBlock16(ref tempInH, ref tempInL, ref tempOutH, ref tempOutL, p, k); uImito ^= tempOutH; } Array.Copy(Dword2Bytes(uImito), imito, 4); }
Сначала идет проверка параметров, затем просто проходим по массиву, вычисляем имитовставку для каждого блока из 8 байт и XORим с предыдущим результатом. Последний неполный блок перед обработкой дополняется нулями.
Фунция зашифрования сообщения в режиме простой замены с одновременной выработкой имитовставки допускает распараллеливание:
public static void EncryptEcbWithImito(byte[] ot, byte[] st, byte[] imito, byte[] k) { Action[] act = { () => Gost28147Ecb(ot, st, k), () => Gost28147Imito(ot, imito, k) }; //Определяем число процессоров (ядер), если >=2 то выполняем параллельно if(Environment.ProcessorCount>=2) { //параллельно Parallel.Invoke(act); } else { //последовательно act[0].Invoke(); act[1].Invoke(); } }
Распараллеливание значительно укоряет процесс обработки сообщения.
Напоследок хотелось бы сказать пару слов о расшифровании. Алгоритм ГОСТ28147-89 спроектирован таким образом, что процесс расшифрования не отличается от процесса зашифрования. Изменяется лишь порядок следования раундовых ключей. Функции расшифрования смотри в прикрепленных исходниках.
Надеюсь было интересно. Будут вопросы, пиши на почту или задавай на форуме

Evgenij,
Update: продолжение темы (вторая часть статьи)
| Вложение | Размер |
|---|---|
| Gost.zip | 41.62 КБ |
- Evgenij's блог
- Войдите или зарегистрируйтесь, чтобы получить возможность отправлять комментарии
- 2431 просмотр



Комментарии
11 комментария(ев)Дата: ЧТ, 13/10/2011 - 23:20
Спасибо, довольно интересно.
Дата: ЧТ, 13/10/2011 - 23:28
Пожалуйста
Дата: ПТ, 14/10/2011 - 05:10
если шифры с нескольками ключами(двумя, тремя). вы можете их опубиковать;-)
Дата: ПТ, 14/10/2011 - 05:44
не совсем понимаю что нужно опубликовать
есть шифры с открытым ключом, у них два ключа. Шифров с тремя ключами нет, есть схемы разделения секрета, в которых какой-либо секрет, например ключ, делится между несколькоми участниками. Например, между 10 участниками, а для восстановления общего секрета необходимо, например, 5 частичных секретов участников, то есть любых человек должны договориться чтоб восстановить общий секрет 
Дата: ПТ, 14/10/2011 - 05:46
Что ж, спасибо! Кстати, мы земляки
Дата: СБ, 15/10/2011 - 00:00
Пожалуйста! Круто!

Update: Кстати кто еще из Минска?
Дата: ПТ, 14/10/2011 - 08:27
2Evgenij
Респект!. Очень интересные темы затрагиваешь в своих статьях
Дата: ПТ, 14/10/2011 - 16:55
Спасибо
Дата: СБ, 15/10/2011 - 03:27
Немогу с вами согласиться, есть промышленные система шифрования с тремя и более ключами шифрование, правдо щас емогу дать вам ссылку на источник но точно помню что есть. прошло время двунаправленых алгоритом т.е. синхроных и асинхроных.
Дата: СБ, 15/10/2011 - 06:43
Криптографические алгоритмы, которые служат для обеспечения конфиденциальности информации, по типу ключей деляться на два класса: симметричные и алгоритмы с открытым ключом. Алгоритмы с открытым ключом, ввиду своей медлительности, прямого применения не нашли. Обычно поступают следующим образом. Вырабатывается случайный сеансовый ключ симметричного алгоритма. На нем шифруется сообщение, а сеансовый ключ шифруется при помощи открытого ключа получателя. Конструкции, наподобие описанной, называются протоколами распределений ключей (в данном случае протокол передачи ключа). Скорее всего ты имеешь ввиду именно протокол
А вообще, если найдешь ссылку то пиши, будет еще одна тема для статьи
Дата: Втр, 07/02/2012 - 02:55
Как раз то, что нужно) Спасибо автору за сохранение многих часов моей жизни. Вот ]]>тут]]> есть много полезного по шифрам под C#.