Режим гаммирования, в отличие от режима простой замены, позволяет шифровать сообщения произвольной длины без применения операции дополнения (паддинга). При этом исходное сообщение может быть разбито на блоки с размером меньшим размера одного блока используемого алгоритма блочного шифрования или равным ему.
В статье мы поговорим о том, как реализуется режим гаммирования, и попробуем написать все необходимые для его реализации функции.
- Блочные алгоритмы шифрования ГОСТ 34.13
- Общие принципы реализации режима гаммирования
- Гамма шифра
- Инициализирующий вектор
- Зашифровываем и расшифровываем
- Пишем необходимые функции
- Ксорим блоки гаммы и текста
- Шифруем строку
- Шифруем файл целиком
- Увеличиваем значение счетчика на единицу
- Удаляем ключи из памяти
- Заключение
Блочные алгоритмы шифрования ГОСТ 34.13
Для начал вот несколько ссылкок, про то, как применять блочные криптоалгоритмы «Кузнечик» и «Магма» для шифрования сообщений, размер которых превышает размер одного блока (для «Кузнечика» он составляет 16 байт, а для «Магмы» — 8 байт) с использованием режима простой замены (ECB, от английского Electronic Codebook). Этот режим описан в ГОСТ 34.13—2015 «Информационная технология. Криптографическая защита информации. Режимы работы блочных шифров». Этот нормативный документ, помимо режима простой замены, определяет еще несколько способов применения блочных шифров, а именно:
- режим гаммирования (CTR, от английского Counter);
- режим гаммирования с обратной связью по выходу (OFB, от английского Output Feedback);
- режим простой замены с зацеплением (CBC, от английского Cipher Block Chaining);
- режим гаммирования с обратной связью по шифртексту (CFB, от английского Cipher Feedback);
- режим выработки имитовставки (MAC, от английского Message Authentication Code).
Что ж, давайте разберемся, как работает гаммирование и как его применять на практике.
Общие принципы реализации режима гаммирования
В настоящее время ГОСТ 34.12—2015 и ГОСТ 34.13—2015 обрели статус межгосударственных (в рамках нескольких государств СНГ) и получили наименования соответственно ГОСТ 34.12—2018 и ГОСТ 34.13—2018. Оба стандарта введены в действие в качестве национальных стандартов Российской Федерации с 1 июня 2019 года.
Гамма шифра
Гаммирование — это наложение (или снятие при расшифровке сообщений) на открытое (или зашифрованное) сообщение так называемой криптографической гаммы. Криптографическая гамма — это последовательность элементов данных, которая вырабатывается с помощью определенного алгоритма. Наложение (или снятие) гаммы на блок сообщения в рассматриваемом нами стандарте реализуется с помощью операции побитного сложения по модулю 2 (XOR). То есть при шифровании сообщений каждый блок открытого сообщения ксорится с блоком криптографической гаммы, длина которого должна соответствовать длине блоков открытого сообщения. При этом, если размер блока исходного текста меньше, чем размер блока гаммы, блок гаммы обрезается до размера блока исходного текста (выполняется процедура усечения гаммы).
Для дешифровки закрытого сообщения необходимо произвести обратную операцию. То есть каждый блок зашифрованного сообщения ксорится с блоком гаммы, и на выходе мы имеем требуемое расшифрованное сообщение.
В большинстве случаев размер блока исходного текста принимается равным размеру блока используемого алгоритма блочного шифрования (напомню, это 16 байт при использовании алгоритма «Кузнечик» или 4 байт при использовании «Магмы»), поэтому процедура усечения блока гаммы может понадобиться только для последнего блока исходного текста, в случае, когда общая длина сообщения не кратна размеру одного блока и последний блок получается неполный.
Чтобы обеспечить высокую стойкость шифрования, блоки гаммы должны отличаться друг от друга, а также иметь случайный (или псевдослучайный) характер. В данном примере блоки различаются благодаря так называемому инициализирующему вектору, значение которого меняется от блока к блоку. Псевдослучайность блоков гаммы при этом реализуется путем шифрования этого вектора с использованием выбранного алгоритма (мы используем «Магму»).
Инициализирующий вектор
Значение этого вектора формируется из так называемой синхропосылки, которая представляет собой число определенной длины. Для режима гаммирования длина этого числа должна быть равна половине размера одного блока используемого алгоритма блочного шифрования. Само значение синхропосылки для режима гаммирования должно быть уникальным для каждого цикла шифрования, проведенного с использованием одинаковых ключей, при этом требований к конфиденциальности синхропосылки не предъявляется (то есть ее можно передавать в открытом виде вместе с зашифрованным сообщением).
РЕКОМЕНДУЕМ:
Поиск энтропии на микросхеме для повышения стойкости шифра
В режиме гаммирования инициализирующий вектор формируется дополнением нулями синхропосылки до размера одного блока используемого алгоритма блочного шифрования. В случае «Магмы» длина синхропосылки равна четырем байтам, длина инициализирующего вектора — восьми. Вторая часть инициализирующего вектора (заполненная нулями) будет использоваться в качестве того самого счетчика (Counter), который и лег в основу англоязычного сокращения CTR.
Очередной блок гаммы шифра получается благодаря шифрованию значения счетчика (его начальное значение равно инициализирующему вектору) с помощью выбранного алгоритма блочного шифрования, при этом после выработки очередного блока гаммы значение счетчика увеличивается на единицу.
Зашифровываем и расшифровываем
В целом процесс шифрования выглядит следующим образом.
На рисунке показаны в том числе и операции усечения гаммы шифра (обозначены буквами T с индексом n). Как я уже говорил, эта операция в большинстве случаев не применяется (когда размеры блоков исходного текста и блоков гаммы шифра совпадают). Операция усечения гаммы шифра может понадобиться для последнего блока (на рисунке обозначена буквой T с индексом s), в случае, когда длина исходного сообщения не кратна размеру блока гаммы шифра.
Дешифровка полностью повторяет процесс шифрования за исключением того, что на вход подают зашифрованный текст, а на выходе получают расшифрованный, который совпадает с исходным.
Пишем необходимые функции
Поскольку процессы шифрования и дешифровки в режиме гаммирования идентичны, определим одну общую функцию для шифрования и расшифровки строки и одну общую функцию для аналогичной работы с файлом.
Ксорим блоки гаммы и текста
Для начала определим функцию сложения блоков по модулю 2.
1 2 3 4 5 6 |
void add_xor(const uint8_t *a, const uint8_t *b, uint8_t *c) { int i; for (i = 0; i < BLCK_SIZE; i++) c[i] = a[i]^b[i]; } |
Здесь все просто и можно, я думаю, обойтись без пояснений.
Шифруем строку
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
void CTR_Crypt(uint8_t *ctr, uint8_t *in_buf, uint8_t *out_buf, uint8_t *key, uint64_t size) { uint64_t num_blocks = size / BLCK_SIZE; // Определяем массив для хранения гаммы uint8_t gamma[BLCK_SIZE]; uint8_t internal[BLCK_SIZE]; uint64_t i; GOST_Magma_Expand_Key(key); for (i = 0; i < num_blocks; i++) // Если очередной блок полный { GOST_Magma_Encrypt(ctr, gamma); // увеличиваем значение счетчика inc_ctr(ctr); memcpy(internal, in_buf + i*BLCK_SIZE, BLCK_SIZE); add_xor(internal, gamma, internal); memcpy(out_buf + i*BLCK_SIZE, internal, BLCK_SIZE); size = size - BLCK_SIZE; } if (size > 0) // Если последний блок неполный { GOST_Magma_Encrypt(ctr, gamma); // увеличиваем значение счетчика inc_ctr(ctr); memcpy(internal, in_buf + i*BLCK_SIZE, size); add_xor(internal, gamma, internal); memcpy(out_buf + num_blocks*BLCK_SIZE, internal, size); size = 0; } GOST_Magma_Destroy_Key(); } |
Здесь GOST\_Magma\_Expand\_Key — это функция развертывания ключей для алгоритма «Магма», а GOST\_Magma\_Encrypt — функция шифрования блока алгоритмом «Магма» из нашей статьи о «Магме». Если для шифрования в режиме гаммирования захочешь использовать «Кузнечик», то эти две функции необходимо заменить на соответствующие функции «Кузнечика» ( GOST\_Kuz\_Expand\_Key и GOST\_Kuz\_Encrypt из статьи про «Кузнечик»).
На вход функции подаются:
- текущее значение инициализирующего вектора (ctr);
- открытое сообщение (in\_buf);
- указатель на буфер для зашифрованного сообщения (out\_buf);
- ключ шифрования (key);
- длина шифруемого сообщения (size).
Шифруем файл целиком
Здесь то же самое. Шифруем и расшифровываем файл одной функцией.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
void CTR_Crypt_File(FILE *src, FILE *dst, uint8_t *init_vec, uint8_t *key, uint64_t size) { uint8_t *in_buf = malloc(BUFF_SIZE); uint8_t *out_buf = malloc(BUFF_SIZE); uint8_t ctr[BLCK_SIZE]; memset(ctr, 0x00, BLCK_SIZE); memcpy(ctr, init_vec, BLCK_SIZE / 2); while (size) { if (size > BUFF_SIZE) { fread(in_buf, 1, BUFF_SIZE, src); CTR_Encrypt(ctr, in_buf, out_buf, key, BUFF_SIZE); fwrite(out_buf, 1, BUFF_SIZE, dst); size -= BUFF_SIZE; } else { fread(in_buf, 1, size, src); CTR_Encrypt(ctr, in_buf, out_buf, key, size); fwrite(out_buf, 1, size, dst); size = 0; } } } |
Здесь на вход функции подаем:
- файл-источник, содержимое которого необходимо зашифровать (src);
- файл, куда будет записано зашифрованное содержимое файла-источника (in\_buf);
- значение синхропосылки (init\_vect);
- ключ шифрования (key);
- размер шифруемого файла (size).
Поскольку размер файла, который нужно зашифровать, может быть большим, его считывание производится не целиком за один раз, а по частям, и константа BUFF\_SIZE как раз и определяет размер буфера, куда будет считана очередная часть.
Увеличиваем значение счетчика на единицу
В функции шифрования строки CTR\_Crypt значение счетчика для выработки очередного блока гаммы шифра увеличивают вызовом функции inc\_ctr. Ее опишем следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
static void inc_ctr(uint8_t *ctr) { int i; unsigned int internal = 0; // Делаем ту самую единичку, на которую увеличиваем счетчик uint8_t bit[BLCK_SIZE]; memset(bit, 0x00, BLCK_SIZE); bit[BLCK_SIZE - 1] = 0x01; // Прибавляем единицу к текущему значению счетчика for (i = BLCK_SIZE - 1; i >= 0; i--) { internal = ctr[i] + bit[i] + (internal >> 8); ctr[i] = internal & 0xff; } } |
Если внимательно посмотреть, то можно увидеть знакомые строчки из функции сложения по модулю n (в данном случае n соответствует размеру блока). С помощью этих строчек к текущему значению счетчика прибавляется массив, по размеру равный одному блоку, в последний байт которого записана единица.
Удаляем ключи из памяти
То место в памяти, где лежат итерационные ключи шифрования, после их использования необходимо очистить. Для этого напишем простую функцию:
1 2 3 4 5 6 |
void GOST_Magma_Destroy_Key() { int i; for (i = 0; i < 32; i++) memset(iter_key[i], 0x00, 4); } |
В нашем случае функция предназначена для работы с «Магмой». Для «Кузнечика», я думаю, ты сможешь написать такую же функцию сам, если понадобится.
Ссылки
- Текст ГОСТ 34.13—2015 можно посмотреть здесь.
- Код для алгоритма «Магма» в виде проекта Qt (в том числе и для режима простой замены).
- Там же ты найдешь исходник для алгоритма «Кузнечик».
Заключение
Теперь вы знаете, как применять блочные алгоритмы шифрования для работы с сообщениями произвольной длины с использованием режима гаммирования. Это позволяет избежать основного недостатка режима прямой замены (если шифруемое сообщение содержит в себе одинаковые или повторяющиеся блоки, то на выходе мы тоже получим зашифрованные одинаковые или повторяющиеся блоки).
РЕКОМЕНДУЕМ:
Что такое и как работает IPsec
В этой статье режим гаммирования рассмотрен применительно к алгоритму «Магма», но если понадобится «Кузнечик», то нет проблем — все необходимые функции мы уже написали.