В этой статье я попробую рассказать вам, дорогие читатели блога, об одной из своих первых попыток написания самодельной программы шифрования файлов и показать вполне рабочий пример простого алгоритма шифрования.
А что это будет за алгоритм и как будет организована программа вы узнаете далее…
На написание данной программы меня подтолкнула книга Брюса Шнайера “Прикладная криптография”, которая содержит очень много интересных и доступных для понимания алгоритмов шифрования с подробным анализом как их механизмов работы, так и возможных слабостей. В одной из глав этой замечательной книги описывается интересный алгоритм под названием 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