После не совсем удачных экспериментов с публикациями на площадках общего назначения приходится вновь возвращаться к теме описания собственных шифров, наиболее значимыми из которых считаю 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);
В текущей версии шифра предусмотрено три основных режима:
простой (он же ECB)
счётчик блоков (для каждого блока применяется постоянно увеличиваемое значение BlNum)
смена ключа с помощью 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 также была отдельная статья.