Создаем свою программу шифрования файлов на RC4

В этой статье я попробую рассказать вам, дорогие читатели блога, об одной из своих первых попыток написания самодельной программы шифрования файлов и показать вполне рабочий пример простого алгоритма шифрования. А что это будет за алгоритм и как будет организована программа вы узнаете далее…

На написание данной программы меня подтолкнула книга Брюса Шнайера «Прикладная криптография», которая содержит очень много интересных и доступных для понимания алгоритмов шифрования с подробным анализом как их механизмов работы, так и возможных слабостей. В одной из глав этой замечательной книги описывается интересный алгоритм под названием RC4, который, прежде всего, заинтересовал меня своей простотой и даже, можно сказать, некоторой прямолинейностью. Именно эти обстоятельства подтолкнули к испытанию RC4 в связке с нашим любимым графической библиотекой QtE5.

Создадим простой dub-проект с минимальными указаниями и отсутствием внешних зависимостей, назовем его simplecrypt. Далее, берем из исходников QtE5 файл qte5.d и переносим его в папку simplecrypt/source проекта.

В simplecrypt мы сделаем простой и минималистичный интерфейс: текстовое поле с кнопкой для выбора файла, который будет шифроваться/дешифроваться, текстовое поле с кнопкой для выбора файла, содержащего ключ, а также несколько кнопок: «Зашифровать», «Расшифровать», «Сгенерировать файл ключа» и «О программе» (кнопка-заглушка, на которую можно повесить генерацию отладочной информации). Все это чудо будет выглядеть примерно так:

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

Добавить комментарий