О реализации этого алгоритма шифрования уже рассказывал FTM: как в общем и целом, так и про режим простой замены. После изучения существующих библиотек и отдельных реализаций этого ГОСТа на C# я решил написать свой велосипед, в первую очередь, ради интереса и опыта. Результатами этой работы мне и хотелось бы поделиться с уважаемым сообществом.
ГОСТ 28147-89 — симметричный блочный алгоритм шифрования с 256-битным ключом, оперирует блоками данных по 64 бита.
Один из режимов его работы, гаммирования с обратной связью, является потоковым режимом блочного шифра.
Обратите внимание, что приведённая последовательность действий справедлива как для шифрования, так и расшифрования. Разница в том, откуда берётся зашифрованный блок текста для обработки следующего блока, это лучше всего видно на картинке:
Данный алгоритм был реализован в форме плагина к менеджеру паролей KeePass.
Исходники доступны на GitHub.
Ниже приведён фрагмент кода класса, реализующего стандартный интерфейс ICryptoTransform, собственно выполняющий криптографическое преобразование данных поблочно. При создании экземпляра в атрибут _state записывается значение синхропосылки, в дальнейшем от направления работы (шифрование или расшифровывание) в него заносится очередной блок зашифрованных данных.
GostECB.Process — реализация того же ГОСТа в режиме простой замены, или «электронной кодовой книги». Хорошее описание алгоритма есть в соответствующем разделе статьи Википедии, а также в статье ГОСТ 28147-89 (Часть 2. Режим простой замены) на Хабрахабре.
Размер снихропосылки, гаммы и «состояния» равен 64 байтам, поэтому шифрование в режиме простой замены можно рассматривать в рамках одного блока. Впрочем, было бы несколько — они просто шифровались бы по очереди.
Для работы с 32-битными частями исходного блока очень удобно использовать тип uint.
Так, в функции F() сложение по модулю ключа и части блока, а также циклический сдвиг на 11 бит запишется просто и лаконично:
Метод подстановки по S-блокам работает с 4-битными кусочками 32-битного подблока, их достаточно удобно отделять побитовым сдвигом и дальнейшим умножением на 0x0f:
Шифрование от расшифровывания в режиме простой замены отличается порядком использования ключей. На самом деле, применительно к режиму гаммирования с обратной связью нам не надо ничего расшифровывать, однако для полноты реализации можно предусмотреть и эту возможность:
ГОСТ 28147-89 — симметричный блочный алгоритм шифрования с 256-битным ключом, оперирует блоками данных по 64 бита.
Один из режимов его работы, гаммирования с обратной связью, является потоковым режимом блочного шифра.
Описание алгоритма
- Исходное сообщение разбивается на блоки по 64 бита
- На каждый блок XOR'ом «накладывается» гамма, тоже длиной 64 бита
- Гамма формируется шифрованием 64-битного блока «состояния» с помощью ключа в режиме простой замены
- В момент начала шифрования сообщения блок принимается равным синхропосылке или вектору инициализации
- В следующей итерации вместо синхропосылки используется зашифрованный блок текста из предыдущей
Обратите внимание, что приведённая последовательность действий справедлива как для шифрования, так и расшифрования. Разница в том, откуда берётся зашифрованный блок текста для обработки следующего блока, это лучше всего видно на картинке:
Реализация
Данный алгоритм был реализован в форме плагина к менеджеру паролей KeePass.
Исходники доступны на GitHub.
Гаммирование с обратной связью
Ниже приведён фрагмент кода класса, реализующего стандартный интерфейс ICryptoTransform, собственно выполняющий криптографическое преобразование данных поблочно. При создании экземпляра в атрибут _state записывается значение синхропосылки, в дальнейшем от направления работы (шифрование или расшифровывание) в него заносится очередной блок зашифрованных данных.
public int TransformBlock (byte[] inputBuffer, int inputOffset, int inputCount, byte[] outputBuffer, int outputOffset)
{
byte[] dataBlock = new byte[inputCount];
byte[] gamma = new byte[GostECB.BlockSize];
byte[] result = new byte[inputCount];
Array.Copy(inputBuffer, inputOffset, dataBlock, 0, inputCount);
gamma = GostECB.Process(_state, _key, GostECB.SBox_CryptoPro_A, true);
result = XOr(dataBlock, gamma);
Array.Copy(result, 0, outputBuffer, outputOffset, inputCount);
Array.Copy(_encrypt ? result : dataBlock, _state, inputCount);
return inputCount;
}
Режим простой замены
GostECB.Process — реализация того же ГОСТа в режиме простой замены, или «электронной кодовой книги». Хорошее описание алгоритма есть в соответствующем разделе статьи Википедии, а также в статье ГОСТ 28147-89 (Часть 2. Режим простой замены) на Хабрахабре.
Размер снихропосылки, гаммы и «состояния» равен 64 байтам, поэтому шифрование в режиме простой замены можно рассматривать в рамках одного блока. Впрочем, было бы несколько — они просто шифровались бы по очереди.
Исходный код метода GostECB.Process
public static byte[] Process(byte[] data, byte[] key, byte[][] sBox, bool encrypt)
{
Debug.Assert(data.Length == BlockSize, "BlockSize must be 64-bit long");
Debug.Assert(key.Length == KeyLength, "Key must be 256-bit long");
var a = BitConverter.ToUInt32(data, 0);
var b = BitConverter.ToUInt32(data, 4);
var subKeys = GetSubKeys(key);
var result = new byte[8];
for (int i = 0; i < 32; i++)
{
var keyIndex = GetKeyIndex(i, encrypt);
var subKey = subKeys[keyIndex];
var fValue = F(a, subKey, sBox);
var round = b ^ fValue;
if (i < 31)
{
b = a;
a = round;
}
else
{
b = round;
}
}
Array.Copy(BitConverter.GetBytes(a), 0, result, 0, 4);
Array.Copy(BitConverter.GetBytes(b), 0, result, 4, 4);
return result;
}
Для работы с 32-битными частями исходного блока очень удобно использовать тип uint.
Так, в функции F() сложение по модулю ключа и части блока, а также циклический сдвиг на 11 бит запишется просто и лаконично:
private static uint F(uint block, uint subKey, byte[][] sBox)
{
block = (block + subKey) % uint.MaxValue;
block = Substitute(block, sBox);
block = (block << 11) | (block >> 21);
return block;
}
Метод подстановки по S-блокам работает с 4-битными кусочками 32-битного подблока, их достаточно удобно отделять побитовым сдвигом и дальнейшим умножением на 0x0f:
private static uint Substitute(uint value, byte[][] sBox)
{
byte index, sBlock;
uint result = 0;
for (int i = 0; i < 8; i++)
{
index = (byte)(value >> (4 * i) & 0x0f);
sBlock = sBox[i][index];
result |= (uint)sBlock << (4 * i);
}
return result;
}
Шифрование от расшифровывания в режиме простой замены отличается порядком использования ключей. На самом деле, применительно к режиму гаммирования с обратной связью нам не надо ничего расшифровывать, однако для полноты реализации можно предусмотреть и эту возможность:
private static int GetKeyIndex(int i, bool encrypt)
{
return encrypt ? (i < 24) ? i % 8 : 7 - (i % 8)
: (i < 8) ? i % 8 : 7 - (i % 8);
}
Источники
- Статья ГОСТ 28147-89 на Википедии
- Описание режимов работы блочных шифров в англоязычной Википедии Block cipher mode of operation
- Исходники реализации ГОСТ 28147-89 Gromila/CryptoAlgorithms на GitHub
- Исходники реализации ГОСТ 28147-89 sftp/gost28147 на GitHub
Комментарии (12)
vasiatka
01.05.2015 23:20Ради интереса… Не хотите попробовать алгоритм «кузнечик»? Данный ГОСТ доживает последние дни, т.к. был теоретически взломан…
Yaruson Автор
01.05.2015 23:35Да, идея идея очень заманчивая, но смущает что стандарт на этот алгоритм пока что черновой и что-то (не знаю что) может поменяться. Вам случайно не известны «большие» его применения, реализованные уже сейчас?
milabs
Гаммирование с обратной связью (CFB) — это потоковый режим работы блочного шифра. Это означает, что в таком режиме работы можно шифровать данные произвольной (байтовой и даже битовой) длины. В вашей реализации, как я вижу, с этим проблема — режим CFB не позволяет обрабатывать не кратные размеру блока.
Yaruson Автор
Действительно, в реализации я упустил этот момент, и зачем-то сделал проверку на длину исходного сообщения. На самом деле ограничение на размер блока актуально только для синхропосылки, которая шифруется в режиме простой замены, а для неполного последнего блока исходных данных просто используется не вся гамма целиком, а только необходимая часть. Спасибо за ценное замечание, поправил.
milabs
Лучше, но всё-равно с ошибкой. Гамма должна проворачиваться только тогда, когда был обработан блок целиком. У вас тут
Array.Copy(_encrypt ? result : dataBlock, _state, inputCount);
вообще что-то непонятное выполняется. Проведите простой тест на корректность — зашифруйте 16 байтов в режиме CFB скармливая функции по одному байту и сравните с тем, что получается при шифровании 2-х блоков.Yaruson Автор
Да, мне не пришло в голову, что можно TransformBlock можно попытаться использовать для неполного блока.
В случае с полными работает верно, не так ли?
Как по вашему мнению, стоит ли следить за количеством обработанных байт и проворачивать гамму только после отработки целого блока, или же просто запретить (плохо звучит, но всё же) обрабатывать TransformBlock неполные блоки?
milabs
Не знаю, я не проверял.
Это зависит от того, что Вы хотите получить и где использовать.
Yaruson Автор
В описании метода TransformBlock из MSDN ничего не сказано про обработку неполных блоков. Можно предположить, что этот метод предназначен исключительно для преобразования полных блоков, тем более, что с неполными работает TransformFinalBlock:
В данной реализации последний блок теперь может быть неполным, это мы исправили в прошлый раз. Как я понимаю, под «partial block» имеется в виду последний неполный блок, а не любой неполный блок в исходном сообщении. Итого получается, что TransformBlock должен работать с полными блоками, а TransformFinalBlock — с полным или неполным.Выходит, можно ограничиться проверкой размера блока в TransformBlock, чтобы формально соблюсти интерфейс ICryptoTransform.
Этот класс предполагается использовать в CryptoStream. К сожалению, я не знаю, как он работает внутри с ICryptoTransform, но полагаю, что он принимает во внимание свойства InputBlockSize и OutputBlockSize, определённые в интерфейсе.
milabs
Здесь я Вам не советчик, руководствуйтесь MSDN.