Хакер - Криптуем по-крупному. Разбираемся с режимом гаммирования из ГОСТ 34.13—2015

Хакер - Криптуем по-крупному. Разбираемся с режимом гаммирования из ГОСТ 34.13—2015

hacker_frei

https://t.me/hacker_frei

Евгений Дроботун

Содержание статьи

  • Общие принципы реализации режима гаммирования
  • Гамма шифра
  • Инициализирующий вектор
  • Зашифровываем и расшифровываем
  • Пишем необходимые функции
  • Ксорим блоки гаммы и текста
  • Шифруем строку
  • Шифруем файл целиком
  • Увеличиваем значение счетчика на единицу
  • Удаляем ключи из памяти
  • Заключение

Режим гаммирования, в отличие от режима простой замены, позволяет шифровать сообщения произвольной длины без применения операции дополнения (паддинга). При этом исходное сообщение может быть разбито на блоки с размером меньшим размера одного блока используемого алгоритма блочного шифрования или равным ему. Сегодня мы поговорим о том, как реализуется режим гаммирования, и напишем все необходимые для его реализации функции.

В одной из статей, посвященных отечественной криптографии, мы разобрались, как применять блочные криптоалгоритмы «Кузнечик» и «Магма» для шифрования сообщений, размер которых превышает размер одного блока (для «Кузнечика» он составляет 16 байт, а для «Магмы» — 8 байт) с использованием режима простой замены (ECB, от английского Electronic Codebook). Этот режим описан в ГОСТ 34.13—2015 «Информационная технология. Криптографическая защита информации. Режимы работы блочных шифров». Этот нормативный документ, помимо режима простой замены, определяет еще несколько способов применения блочных шифров, а именно:

  • режим гаммирования (CTR, от английского Counter);
  • режим гаммирования с обратной связью по выходу (OFB, от английского Output Feedback);
  • режим простой замены с зацеплением (CBC, от английского Cipher Block Chaining);
  • режим гаммирования с обратной связью по шифртексту (CFB, от английского Cipher Feedback);
  • режим выработки имитовставки (MAC, от английского Message Authentication Code).

Что ж, давай разберемся, как работает гаммирование и как его применять на практике.

INFO

В настоящее время ГОСТ 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.

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];
}

Здесь все просто и можно, я думаю, обойтись без пояснений.

Шифруем строку

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).

Шифруем файл целиком

Здесь то же самое. Шифруем и расшифровываем файл одной функцией.

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. Ее опишем следующим образом:

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 соответствует размеру блока). С помощью этих строчек к текущему значению счетчика прибавляется массив, по размеру равный одному блоку, в последний байт которого записана единица.

Удаляем ключи из памяти

То место в памяти, где лежат итерационные ключи шифрования, после их использования необходимо очистить. Для этого напишем простую функцию:

void GOST_Magma_Destroy_Key()
{
  int i;
  for (i = 0; i < 32; i++)
    memset(iter_key[i], 0x00, 4);
}

В нашем случае функция предназначена для работы с «Магмой». Для «Кузнечика», я думаю, ты сможешь написать такую же функцию сам, если понадобится.

Заключение

Теперь ты знаешь, как применять блочные алгоритмы шифрования для работы с сообщениями произвольной длины с использованием режима гаммирования. Это позволяет избежать основного недостатка режима прямой замены (если шифруемое сообщение содержит в себе одинаковые или повторяющиеся блоки, то на выходе мы тоже получим зашифрованные одинаковые или повторяющиеся блоки). В этой статье режим гаммирования рассмотрен применительно к алгоритму «Магма», но если понадобится «Кузнечик», то нет проблем — все необходимые функции мы уже написали.

Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei

Report Page