В этой статье я попробую рассказать вам, дорогие читатели блога, об одной из своих первых попыток написания самодельной программы шифрования файлов и показать вполне рабочий пример простого алгоритма шифрования.
А что это будет за алгоритм и как будет организована программа вы узнаете далее…
На написание данной программы меня подтолкнула книга Брюса Шнайера «Прикладная криптография», которая содержит очень много интересных и доступных для понимания алгоритмов шифрования с подробным анализом как их механизмов работы, так и возможных слабостей. В одной из глав этой замечательной книги описывается интересный алгоритм под названием RC4, который, прежде всего, заинтересовал меня своей простотой и даже, можно сказать, некоторой прямолинейностью. Именно эти обстоятельства подтолкнули к испытанию RC4 в связке с нашей любимой графической библиотекой QtE5.
Создадим простой dub-проект с минимальными указаниями и отсутствием внешних зависимостей, назовем его simplecrypt. Далее, берем из исходников QtE5 файл qte5.d и переносим его в папку simplecrypt/source проекта.
В simplecrypt мы сделаем простой и минималистичный интерфейс: текстовое поле с кнопкой для выбора файла, который будет шифроваться/дешифроваться, текстовое поле с кнопкой для выбора файла, содержащего ключ, а также несколько кнопок: «Зашифровать», «Расшифровать», «Сгенерировать файл ключа» и «О программе» (кнопка-заглушка, на которую можно повесить генерацию отладочной информации). Все это чудо будет выглядеть примерно так:
Этот интерфейс поместим в файл gui.d. Также очевидно, что потребуется четыре «переходника» под события всех кнопок, что все вместе выглядит следующим образом:
extern(C) { void onLoadFile(MainForm* mainFormPointer) { (*mainFormPointer).runLoadFile; } void onLoadKeyFile(MainForm* mainFormPointer) { (*mainFormPointer).runLoadKeyFile; } void onEncryptFile(MainForm* mainFormPointer) { (*mainFormPointer).runEncryptFile; } void onDecryptFile(MainForm* mainFormPointer) { (*mainFormPointer).runDecryptFile; } void onGenerateKeyFile(MainForm* mainFormPointer) { (*mainFormPointer).runGenerateKeyFile; } void onAbout(MainForm* mainFormPointer) { (*mainFormPointer).runAbout; } } // псевдонимы под Qt'шные типы alias WindowType = QtE.WindowType; // основное окно class MainForm : QWidget { private { QVBoxLayout mainBox, vertical0; QHBoxLayout horizontal0, horizontal1, horizontal2, horizontal3; QGroupBox box0, box1, box2; QPushButton button0, button1, button2, button3, button4, button5; QLineEdit edit0, edit1; QAction action0, action1, action2, action3, action4, action5; } this(QWidget parent, WindowType windowType) { super(parent, windowType); setMaximumSize(500, 250); setFixedWidth(500); setFixedHeight(250); setWindowTitle("RC4 Crypter"); mainBox = new QVBoxLayout(this); horizontal0 = new QHBoxLayout(this); edit0 = new QLineEdit(this); edit0.setReadOnly(true); button0 = new QPushButton("Select...", this); action0 = new QAction(null, &onLoadFile, aThis); connects(button0, "clicked()", action0, "Slot()"); horizontal0 .addWidget(edit0) .addWidget(button0); box0 = new QGroupBox(this); box0.setText("File for encryption/decription:"); box0.setMaximumHeight(55); box0.setLayout(horizontal0); horizontal1 = new QHBoxLayout(this); edit1 = new QLineEdit(this); edit1.setReadOnly(true); button1 = new QPushButton("Select...", this); action1 = new QAction(null, &onLoadKeyFile, aThis); connects(button1, "clicked()", action1, "Slot()"); horizontal1 .addWidget(edit1) .addWidget(button1); box1 = new QGroupBox(this); box1.setText("Key file:"); box1.setMaximumHeight(55); box1.setLayout(horizontal1); vertical0 = new QVBoxLayout(this); horizontal2 = new QHBoxLayout(this); button2 = new QPushButton("Encrypt", this); action2 = new QAction(null, &onEncryptFile, aThis); connects(button2, "clicked()", action2, "Slot()"); button3 = new QPushButton("Decrypt", this); action3 = new QAction(null, &onDecryptFile, aThis); connects(button3, "clicked()", action3, "Slot()"); horizontal2 .addWidget(button2) .addWidget(button3); horizontal3 = new QHBoxLayout(this); button4 = new QPushButton("Generate key file", this); action4 = new QAction(null, &onGenerateKeyFile, aThis); connects(button4, "clicked()", action4, "Slot()"); button5 = new QPushButton("About...", this); action5 = new QAction(null, &onAbout, aThis); connects(button5, "clicked()", action5, "Slot()"); horizontal3 .addWidget(button4) .addWidget(button5); vertical0 .addLayout(horizontal2) .addLayout(horizontal3); box2 = new QGroupBox(this); box2.setText("Crypting option:"); box2.setLayout(vertical0); mainBox .addWidget(box0) .addWidget(box1) .addWidget(box2); setLayout(mainBox); }
В принципе, ничего из этого не является для читателей нашего блога новым, так как построение интерфейса вручную (иных методов пока нет) описывалось в нескольких статьях нашего блога. Ограничим размеры окна приложения, чтобы оно выглядело аккуратно и при расширении на весь экран не смогло поменять свои размеры, иначе, элементы интерфейса будут выглядеть не эстетично.
setMaximumSize(500, 250); setFixedWidth(500); setFixedHeight(250);
Теперь напишем обработчики нажатия кнопок, и прежде всего выбор файла для обработки нашим шифровальным приложением, а также выбор файла ключа:
void runLoadFile() { QFileDialog fileDialog = new QFileDialog('+', null); string filename = fileDialog.getOpenFileNameSt("Open file for encryption/decryption", "", "*.*"); edit0.setText(filename); } void runLoadKeyFile() { QFileDialog fileDialog = new QFileDialog('+', null); string filename = fileDialog.getOpenFileNameSt("Open key file", "", "*.key *.keyfile *.rc4"); edit1.setText(filename); }
Эти обработчики, помещенные в класс окна, просто получают путь до соответствующего файла и отображают его в нужное текстовое поле, предварительно защищенное от записи данных со стороны пользователя. Также, мы устанавливаем ограничение на расширение файла, разрешаем следующий набор: *.key, *.keyfile, *.rc4.
Далее определим другой обработчик, который обсчитывает нажатие кнопки «Сгенерировать ключ». По моей задумке, эта кнопка генерирует случайный файл ключа, помещая в него набор из 256 случайных байтов, выбранных из источника псевдослучайных чисел. Реализация этой идеи довольно простая, но нам также требуется уникальное имя файла, и такое, чтобы оно внезапно не совпало с некоторым уже существующим. Необходимое имя файла можно получить вводом соглашения об именовании файла ключа: пусть имя файла ключа будет начинаться с префикса key_ за которым последует дата его создания с точностью до секунд.
Таким образом, наш обработчик выглядит примерно так:
void runGenerateKeyFile() { import std.datetime; import std.random; import std.stdio; import std.string; auto filename = Clock.currTime .toString .replace(":", "") .replace("-", "_") .replace(".", "_") .replace(" ", "_"); try { File file; file.open("key_%s.rc4".format(filename), "wb"); Random random = Random(unpredictableSeed); for (int i = 0; i < 256; i++) { file.write(cast(char) uniform(0, 256, random)); } file.close; msgbox( "File key_%s.rc4 has been generated".format(filename), "RC4 Key Generation", QMessageBox.Icon.Information ); } catch { msgbox( "Key file generation failed", "RC4 Key Generation", QMessageBox.Icon.Critical ); } }
Для выполнения введенного соглашения используем получение текущего времени и заменим некоторые символы строки времени на более корректные, приводящие к вполне читаемому и удобному имени файла. Также, введем страховочный механизм, который в случае невозможности создания файла ключа выведет окно с сообщением об ошибке, и это не приведет к краху всего приложения. В случае успешного создания файла ключа, программа выдаст окно о том, что нужный файл создан и покажет его название.
Остальные два обработчика на время оставим в покое, поскольку они потребуют уже подготовленного кода, который мы сейчас рассмотрим.
Прежде всего, напишем сам алгоритм RC4 (описание и механизм работы я приводить не буду, он есть в упомянутой мной книге или в википедии), реализуя его в виде такого класса:
module simplecrypt.rc4; private { import std.string; } class RC4 { private { ubyte[256] S_Box; ubyte[256] Key; // перестановка элементов void exchange(ref ubyte[256] s, int i, int j) { ubyte tmp = s[j]; s[j] = s[i]; s[i] = tmp; } // инициализация перестановочной таблицы auto initializeSBox() { foreach (i, ref e; S_Box) { e = cast(ubyte) i; } auto j = 0; foreach (i; 0..256) { j = (j + S_Box[i] + Key[i]) % 256; exchange(S_Box, i, j); } } } this(){} // ввести 256-байтный ключ auto adjustKey(ubyte[256] key) { Key = key; initializeSBox; } // зашифровать поток битов ubyte[] encrypt(inout(ubyte[]) bytes) { auto i = 0; auto j = 0; auto S = S_Box; ubyte[] accumulator; foreach (unit; bytes) { i = (i + 1) % 256; j = (j + S[i]) % 256; exchange(S, i, j); int t = (cast(int) S[i] + cast(int) S[j]) % 256; ubyte k = S[t]; accumulator ~= unit ^ k; } return accumulator; } // зашифровать строку string encrypt(string text) { string accumulator; foreach(unit; this.encrypt(text.representation)) { accumulator ~= cast(char) unit; } return accumulator; } }
Класс RC4 работает следующим образом: сначала создается объект класса, затем методом adjustKey устанавливается 256-байтный ключ (задается в виде массива байтов), а затем по необходимости, методом encrypt шифруется или дешифруется набор байтов или строка. Стоит заметить, что метода обратного encrypt нет и расшифровывание файла выполняется с помощью того же самого метода encrypt на вход которому подается зашифрованный блок (а по необходимости повторяется процедура установки ключа). Сам класс RC4 мы помещаем в файл rc4.d.
Чтобы пойти дальше, нам потребуется принять тот факт, что исходный файл и файл ключа не должны быть изменены нашей программой, и что пользователь сам должен решить что делать с незашифрованным файлом. Понимаю, что это один из неразумных шагов с моей стороны, однако, я пока не знаю, как уничтожить исходный файл безопасным образом. Кроме того, неизменность обоих файлов в начале процесса обработки их через simplecrypt вполне надежная гарантия успешной работы алгоритма. Стало быть файлы должны быть read-only, т.е. доступны только для чтения.
Реализуем свою абстракцию «защиты файлов» в файле readonlyfile.d:
module simplecrypt.readonlyfile; private { import std.file; import std.path; import std.range; import std.stdio; import std.string; } class ReadOnlyFile { private { void[] fileContent; } this(){} auto open(string filepath) { if (filepath.exists) { if (filepath.isFile) { fileContent = std.file.read(filepath); if (fileContent.length == 0) { throw new FileException("File %s is empty".format(filepath)); } } else { throw new FileException("Filesystem's object %s is not a file".format(filepath)); } } else { throw new FileException("Filesystem's object %s does not exists".format(filepath)); } } void[] read() { if (fileContent.empty) { throw new FileException("Trying of access to unopened file"); } else { return fileContent; } } void close() { if (fileContent.empty) { throw new FileException("Trying of access to unopened file"); } else { fileContent = []; } } ~this() { if(!fileContent.empty) { fileContent = []; } } }
Как видно из приведенного выше фрагмента кода, мы постарались учесть все возможные преграды на пути извлечения из файлов информации в виде байтового потока, снабдив наш класс блоками генерации исключительных ситуаций с подробной информацией о возникших проблемах.
Теперь можно описать процедуру, которая применяет алгоритм RC4 к некоторому файлу:
module simplecrypt.encrypt; private { import std.algorithm; import std.range; import std.stdio; import simplecrypt.rc4; import simplecrypt.readonlyfile; } auto encryptFile(string sourceFileName, string keyFileName, string targetFileName) { ReadOnlyFile sourceFile = new ReadOnlyFile; sourceFile.open(sourceFileName); auto text = cast(ubyte[]) sourceFile.read; sourceFile.close; ReadOnlyFile keyFile = new ReadOnlyFile; keyFile.open(keyFileName); ubyte[256] key = (cast(ubyte[]) keyFile.read).array; keyFile.close; RC4 rc4 = new RC4; rc4.adjustKey(key); auto result = rc4.encrypt(text); File file; file.open(targetFileName, "wb"); result .map!(a => cast(char) a) .each!(a => file.write(a)); file.close; }
Идея кода проста: открываем исходный файл и файл ключа, считываем их в байтовые массивы, инициализируем класс RC4 и заполняем его массивом байтов ключа, применяем алгоритм шифрования. Далее, открываем новый файл (его имя задается третьим аргументом функции encryptFile, первые два — это имя исходного и ключевого файла соответственно), переводим байтовое представление в символьное и записываем в открытый файл, после чего производим его закрытие.
Помещаем всю процедуру в файл encrypt.d, и возвращаемся к файлу gui.d, в который мы теперь можем дописать два обработчика. Первый обработчик, который срабатывает по кнопке «Зашифровать» делает следующее: получает пути нужных нам файлов из текстовых полей, затем проверяет введенные данные, и в случае их корректности, вызывает процедуру encryptFile и записывает полученный от нее поток байтов в новый файл, который имеет тоже имя что и старый, однако, расширение сменено на *.crypted:
void runEncryptFile() { auto sourceFileName = edit0.text!string; auto keyFileName = edit1.text!string; if ((sourceFileName != "") && (keyFileName != "")) { try { auto targetFileName = sourceFileName ~ ".crypted"; setWindowTitle("RC4 encrypting ..."); encryptFile(sourceFileName, keyFileName, targetFileName); setWindowTitle("Done."); Thread.sleep(250.msecs); setWindowTitle("RC4 Crypter"); msgbox( "File %s has been encrypted".format(edit0.text!string), "RC4 Encryption", QMessageBox.Icon.Information ); } catch { msgbox( "Unable to crypt file %s".format(edit0.text!string), "RC4 Encryption", QMessageBox.Icon.Critical ); } } else { msgbox( "File for encryption and/or key file not found", "RC4 Encryption", QMessageBox.Icon.Critical ); } }
Нетрудно заметить, что предприняты меры предосторожности на случай некорректных данных, а также специально введено замедление процесса для того, чтобы отобразить информацию о ходе процесса шифрования.
Обработчик кнопки «Расшифровать» выглядит почти идентично (процедура расшифровки та же самая, что и для зашифровки), но в ходе своей работы он убирает расширение *.crypted:
void runDecryptFile() { auto sourceFileName = edit0.text!string; auto keyFileName = edit1.text!string; if((sourceFileName != "") && (keyFileName != "")) { try { auto targetFileName = sourceFileName.replace(".crypted", ""); setWindowTitle("RC4 decrypting ..."); encryptFile(sourceFileName, keyFileName, targetFileName); setWindowTitle("Done."); Thread.sleep(250.msecs); setWindowTitle("RC4 Crypter"); msgbox( "File %s has been decrypted".format(edit0.text!string), "RC4 Decryption", QMessageBox.Icon.Information ); } catch { msgbox( "Unable to crypt file %s".format(edit0.text!string), "RC4 Decryption", QMessageBox.Icon.Critical ); } } else { msgbox( "File for decryption and/or key file not found", "RC4 Encryption", QMessageBox.Icon.Critical ); } }
Теперь наш небольшой проект готов к сборке, а также ко всевозможным интересным экспериментам…
Например, однажды мне почти удалось зашифровать саму программу шифровальщик, что привело к весьма занятным результатам и выводам: при попытке запуска программы с включенным антивирусом можно получить блокировку приложения, т.к. как антивирус считает, что у нас в системе запущен полиморфный вирус.
На этом прекрасном моменте, я заканчиваю свой рассказ и прилагаю к этой статье полный код всего проекта с примерами сгенерированных ключей.
Архив с проектом: скачать simplecrypt