После не совсем удачных экспериментов с публикациями на площадках общего назначения приходится вновь возвращаться к теме описания собственных шифров, наиболее значимыми из которых считаю ESCK-7 и Seal. Оба шифра являются родственными и отличаются только некоторыми деталями, потому основное описание предполагается посвятить шифру ESCK-7, а для Seal указать только принципиальные различия.

ESCK-7

Итак, ESCK-7 является алгоритмом блочного шифрования собственной разработки, который оперирует 64-битными целыми числами без знака и работает с блоками по 2 и ключом 256 таких чисел. Три основных функции алгоритма: EncryptBlock и DecryptBlock для шифрования и расшифровки блока, а также GFunc, нужная для генерации или изменения ключа (выполняет что-то похожее на необратимое шифрование ключа самим собой).

EncryptBlock работает следующим образом. Для каждого из двух чисел блока вычисляется 64-битная адресная переменная Adr, которая зависит от второго числа в блоке, опционального номера блока и одного из чисел ключа (определяется по промежуточному состоянию Adr). Далее эта переменная разбивается на восемь 8-битных номеров, по которым и берутся числа ключа для шифрования текущего числа блока Mi.  Само шифрование выполняется с помощью операций сложения, XOR, NOT и циклических сдвигов (ROL64) над самим Mi и выбранными числами ключа. Подобное шифрование повторяется на протяжении нескольких циклов (их количество не является фиксированным и указывается вручную) и номер цикла также влияет на шифрование. Код функции для массива блока Block= M приведён ниже.

void ESCK7::EncryptBlock(uint64_t* M, const uint64_t* K, int Cycles,
uint64_t BlNum)
{
	// исключение в случае нулевого указателя
	if (M == nullptr || K == nullptr)
		throw std::invalid_argument("Нулевой указатель на блок или ключ");
	
	// многоцикличное шифрование
	for (int C = 1; C <= Cycles; C++)
	{
		// основной цикл шифрования
		for (int i = 0; i < 2; i++)
		{
			// копирование текущего M
			uint64_t Mi = M[i];
			
			// вычисление адресной переменной
			uint64_t Adr = ROL64(M[(i + 1) & 1] + BlNum, 37);
			Adr = ROL64(Adr + K[Adr & 0xff], 43);
			
			// последовательное преобразование Mi
			Mi += ROL64(~Adr + M[(i + 1) & 1], 15);
			Mi += ROL64(K[Adr & 0xff], 11); Adr >>= 8;
			Mi += ROL64(K[Adr & 0xff], 26); Adr >>= 8;
			Mi += ~ROL64(K[Adr & 0xff], 10); Adr >>= 8;
			Mi = ROL64(Mi, 39) ^ (~K[Adr & 0xff]); Adr >>= 8;
			Mi += ROL64(K[Adr & 0xff], 25); Adr >>= 8;
			Mi += ROL64(K[Adr & 0xff], 3); Adr >>= 8;
			Mi += ROL64(~K[Adr & 0xff], 29); Adr >>= 8;
			Mi = ROL64(Mi ^ K[Adr & 0xff], 54) + C;
			
			// возврат Mi в рабочий массив
			M[i] = Mi;
		}
	}
}

Функция GFunc похожа на шифрование блока, но разница в том, что шифруются не числа блока, а сами числа ключа K (все 256 по очереди) и шифрование предполагается необратимым: на вычисление Adr влияют не только «соседи», но и само шифруемое число. Также в функции применяются другие величины циклических сдвигов в ROL64. Код функции выглядит следующим образом:

void ESCK7::GFunc(uint64_t* K, int Cycles)
{
	// исключение в случае нулевого указателя
	if (K == nullptr)
		throw std::invalid_argument("Нулевой указатель K");
	
	// многоцикличная генерация
	for (int C = 1; C <= Cycles; C++)
	{
		// основной цикл шифрования
		for (int i = 0; i < 256; i++)
		{
			// скопировать текущий K
			uint64_t Ki = K[i];
			
			// вычислить адресную переменную
			uint64_t Adr = ROL64(Ki, 7) ^ K[(i + 1) & 0xff];
			Adr = ROL64(Adr + K[(i + 255) & 0xff], 17);
			
			// последовательное преобразование Ki 
			Ki += ROL64(K[(i + 255) & 0xff], 5) ^ (~Adr);
			Ki += ROL64(K[Adr & 0xff], 14); Adr >>= 8;
			Ki += ~ROL64(K[Adr & 0xff], 27); Adr >>= 8;
			Ki += ROL64(K[Adr & 0xff], 3); Adr >>= 8;
			Ki = ROL64(Ki, 33) ^ (~K[Adr & 0xff]); Adr >>= 8;
			Ki += ROL64(K[Adr & 0xff], 19); Adr >>= 8;
			Ki += ROL64(K[Adr & 0xff], 51); Adr >>= 8;
			Ki += ROL64(~K[Adr & 0xff], 44); Adr >>= 8;
			Ki = ROL64(Ki ^ K[Adr & 0xff], 2) + C;
			
			// возвратить Ki обратно в ключ
			K[i] = Ki;
		}
	}
}

В алгоритме также реализована встроенная генерация ключа по байтовому массиву (KeyGen, KeyGenData) или по паролю из символов ASCII (KeyGenA), причём генерация возможна как с предварительной инициализацией ключа, так и без неё, что позволяет генерировать ключ по нескольким массивам и/или паролям. Суть генерации следующая: массив или символы пароля побайтно добавляются к ключу K с помощью операции XOR и выполняется несколько циклов функции GFunc для перемешивания связей. Чтобы ничего не потерялось, XOR и GFunc повторяется несколько раз, а затем выполняется финальный GFunc с заданным количеством циклов. Нужно только учесть различия между добавлением байтов массива и символов пароля: байты объединяются группами по 8 в 64-битные числа, которые и XOR-рятся с числами K, а символы добавляются каждый к отдельному K[i], чтобы ASCII символы (коды 0..127) добавлялись одинаково независимо от кодировки: utf-8, utf-16, utf-32.

Другими полезными функциями являются функции шифрования и расшифровки байтовых массивов в виде std::vector<char> или std::vector<uint8_t> (функции Encrypt и Decrypt), а также в виде указателей void* (EncryptData и DecryptData). Функции позволяют разбить указанные массивы на отдельные блоки и шифровать каждый из них текущим ключом после его генерации. Ниже приведён простой пример генерации ключа и шифрования массива:

// данные и "соль"
uint8_t Data[N];
uint8_t Salt[16];

// шифр с многоразовым ключом
ESCK7 Cipher;
Cipher.KeyGenA("Password", Cycles, true); // true - инициализация ключа

// шифр с одноразовым ключом
ESCK7 Cip = Cipher;
Cip.KeyGenData(Salt, 16, Cycles, false); // false - без инициализации

// шифрование/расшифровка данных
Cip.EncryptData(Data, N);

В текущей версии шифра предусмотрено три основных режима:

  1. простой (он же ECB)

  2. счётчик блоков (для каждого блока применяется постоянно увеличиваемое значение BlNum)

  3. смена ключа с помощью GFunc после шифрования каждого блока

Режим устанавливается с помощью функции SetMode(int Md), где Md = 0 соответствует простому режиму, а для счётчика блоков и смены ключа предусмотрены константы mdBlCnt и mdChngKey.

Seal

Шифр Seal (от Simple Encrypting Algorithm — простой алгоритм шифрования) реализует немного другой подход: вместо секретного ключа K в шифровании участвуют числа шифрующей таблицы T, а 128-битный ключ Key применяется отдельно. В шифровании блока основная разница заключается в вычислении адресной переменной Adr:

// вычисление адресной переменной
uint64_t Adr = ROL64(M[(i + 1) & 1] + BlNum, 37);
Adr = ROL64(Adr + Key[i], 43);

Для шифрования 256-битными ключами предполагается эти ключи делить на две 128-битные части Key1 и Key2 и шифровать этими частями по очереди: Key1, Key2, Key1.

Поскольку алгоритм простой, он реализован только в виде функций шифрования и расшифровки блока, а генерация ключа, шифрование массивов и прочее предполагается реализовать дополнительно.

Заключение

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

В случае интереса эволюцию шифров ESCK можно проследить по их прежним версиям в архиве, начиная от первого алгоритма «кодирования-декодирования». Об алгоритме ESCK-6 также была отдельная статья.

Комментарии (0)