Содержание статьи
Задание: разработать библиотеку классов, реализующих криптографический алгоритм
AES (Advanced Encryption Standard). Предусмотреть следующие варианты шифровки/дешифровки:
а) шифрация данных из файла в файл;
б) дешифрация данных из файла в файл;
в) дешифрация данных из файла в память.
Ключ шифрования должен задаваться строкой, содержащей последовательность шестнадцатеричных цифр. Длина ключа - 128, 192 или 256 бит.
Введение в проблему
Advanced Encryption Standard (AES), также известный как
RIJNDAEL, — это симметричный алгоритм блочного шифрования (размер блока - 128 бит, ключ - 128/192/256 бит), выбранный в ходе конкурса и принятый в качестве американского стандарта шифрования правительством США. Выбор был сделан с расчетом на повсеместное использование и активный анализ алгоритма, как это было с его предшественником
DES. Государственный институт стандартов и технологий США (National Institute of Standards and Technology, NIST), после пятилетней подготовки, 26 ноября 2001 года опубликовал предварительную
спецификацию AES, а 26 мая 2002 года AES был объявлен стандартом. По состоянию на 2006 год
AES является одним из самых распространенных алгоритмов симметричного шифрования в мире.
История AES
В далеком 1998 году NIST объявил конкурс на создание алгоритма, удовлетворяющего выдвинутым институтом требованиям. Он опубликовал все несекретные данные о тестировании кандидатов на роль
AES и потребовал от авторов алгоритмов сообщить о базовых принципах построения используемых в них констант. В отличие от ситуации с DES, NIST при выборе
AES не стал опираться на секретные и, как следствие, запрещенные к публикации данные об исследовании алгоритмов-кандидатов.
Чтобы быть утвержденным в качестве стандарта, алгоритм должен был:
- реализовать шифрование частным ключом;
- представлять собой блочный шифр;
- работать со 128-разрядными блоками данных и ключами трех размеров (128, 192 и 256 разрядов).
Дополнительно кандидатам рекомендовалось:
- использовать операции, легко реализуемые как аппаратно (в микрочипах), так и программно (на персональных компьютерах и серверах);
- ориентироваться на 32-разрядные процессоры;
- не усложнять без необходимости структуру шифра для того, чтобы все заинтересованные стороны были в состоянии самостоятельно провести независимый криптоанализ алгоритма и убедиться, что в нем не заложено каких-либо недокументированных возможностей.
Кроме того, алгоритм, претендующий на роль стандарта, должен распространяться по всему миру на неэксклюзивных условиях и без платы за пользование патентом.
Перед первым туром конкурса в NIST поступило 21 предложение, 15 из которых соответствовали выдвинутым критериям. Затем были проведены исследования этих решений, в том числе связанные с дешифровкой и проверкой производительности, и получены экспертные оценки специалистов по криптографии. В августе 1999 года NIST объявил пять финалистов, которые получили право на участие во втором этапе обсуждений.
2 октября 2000 года NIST сообщил о своем выборе – победителем конкурса стал алгоритм
RIJNDAEL (произносится как «райндол») бельгийских криптографов
Винсента Раймана и Йоана Дамана, который зарегистрирован в качестве официального федерального стандарта как
FIPS 197 (Federal Information Processing Standard).
Для меня остается загадкой, зачем в российском вузе преподают стандарты иностранных государств. Видимо, исходят из принципа, что врага надо знать в лицо :). Ладно, в общем-то, это не наше дело. Нам надо просто программно реализовать основу национальной безопасности США.
Начало работы
Итак, приступим. Так как по заданию нам надо было разработать библиотеку классов, то начнем с описания этих классов, то есть с заголовочных файлов. Наш класс будет иметь три метода, которые реализуют основные операции, перечисленные в задании: encryptFile - шифрация данных из файла в файл, DecryptToFile - дешифрация данных из файла в файл, DecryptToMemory - дешифрация данных из файла в память. Собственно, в заголовочном файле нет ничего особенного, поэтому просто приведу его листинг с комментариями (смотри листинг 1).
#define BLOCK_LEN 16
class CAes
{
public:
CAes(void);
~CAes(void);
/*! \brief Расшифровывает файл в память
* Определяет размер файла и выделяет память в buffer
* \return Число расшифрованных байт или число <= 0, в случае ошибки
*/
static int DecryptToMemory(std::string fileName, std::string key, unsigned char* &buffer);
/* \brief Создает файл outFileName и записывает в него зашифрованное содержимое inFileName
* \return Число зашифрованных байт
*/
static int encryptFile(std::string inFileName, std::string outFileName, std::string key);
/* \brief Расшифровывает из файла в файл
* \return Число расшифрованных байт
*/
static int DecryptToFile(std::string inFileName, std::string outFileName, std::string key);
};
Как видно из листинга, мы определяем всего три вышеперечисленных метода и конструктор с деструктором. Теперь займемся реализацией этих методов.
Вспомогательный код
Прежде чем приступить непосредственно к выполнению трех основных пунктов задания, надо подготовиться, то есть написать пару вспомогательных функций и подключить несколько заголовочных файлов. Эти файлы мы нашли в интернете, ведь ты не думал, что мы будем с нуля реализовывать криптографический госстандарт США. В этих файлах содержится куча полезных структур и алгоритмов, которые позволяют заниматься разработкой непосредственно класса, а не вникать в сложную
математику AES. В большинстве случаев преподаватели сами предоставляют подобные куски кода, чтобы облегчить и без того тяжелую жизнь студента. Все эти файлы ты найдешь (а при желании и детально ознакомишься с их содержанием) на нашем
DVD.
Взглянем на листинг. Там как раз описаны две наши вспомогательные функции и один макрос.
void FillRand(char *buf, const int len)
{
static unsigned long a[2], mt = 1, count = 4;
static char r[4];
int i;
if(mt) { mt = 0; cycles((unsigned __int64 *)a); }
for(i = 0; i < len; ++i)
{
if(count == 4)
{
*(unsigned long*)r = RAND(a[0], a[1]);
count = 0;
}
buf[i] = r[count++];
}
}
Все это хозяйство служит для заполнения массива псевдослучайных чисел. Непосредственно же заполнением занимается эта функция:
void FillRand(char *buf, const int len)
Как ее использовать, ты узнаешь чуть позже. Макрос RAND(a,b) вычисляет псевдослучайное число, а функция cycles считывает число из таймера процессора. Все это войдет в cpp-файл с реализацией класса. Также нам надо включить еще три заголовочных файла:
#include "caes.h" #include "aes.h" #include <stdio.h>
Здесь, caes.h описывает наш класс; aes.h – один из тех файлов, что мы нашли в интернете; stdio.h - говорить стыдно, все и так должны знать, что это стандартный ввод/вывод в
C.
Шифрование файла
Настало время заняться функцией шифрования. Прототип ее выглядит так:
int encryptFile(std::string inFileName, std::string outFileName, std::string secretKey)
Содержимое из файла inFileName будет шифроваться ключом secretKey и записываться в файл outFileName. Код этой функции частично представлен в листинге.
unsigned long i = 0;
int by = 0, key_len, err = 0;
const char* cKey = secretKey.c_str();
char ch,key[32];
const char* cp = &cKey[0]; // указатель на символ в key
i = 0; // счетчик обработанных цифр
while(i < 64 && *cp) // максимальная длина ключа - 32 байта и,
{ // следовательно, максимум 64 шестнадцатеричные цифры
ch = toupper(*cp++);
if(ch >= '0' && ch <= '9')
by = (by << 4) + ch - '0';
else if(ch >= 'A' && ch <= 'F')
by = (by << 4) + ch - 'A' + 10;
else // ошибка, если символ не шестнадцатеричная цифра
return -2;
// запоминаем байт ключа для каждой пары шестнадцатеричных цифр
if(i++ & 1)
key[i / 2 - 1] = by & 0xff;
}
if(*cp)
return -3;
else
if(i < 32 || (i & 15))
return -4;
key_len = i / 2;
int encrypted = 0;
FILE* inFile = fopen(inFileName.c_str(), "rb");
if (!inFile)
return -5;
FILE* outFile = fopen(outFileName.c_str(), "wb");
if (!outFile)
return -5;
aes_encrypt_ctx ctx[1];
aes_encrypt_key((unsigned char*)key, key_len, ctx);
/* далее идет непосредственно алгоритм шифрования; в этом листинге он опускается по причине громоздкости; полную версию функции можно увидеть на диске, прилагаемом к журналу, в файле caes.cpp */
// закрываем файлы, чтобы сохранить информацию в них
fclose(inFile);
fclose(outFile);
return 0;
}
Весь код, к сожалению, мы привести не можем, поскольку он занял бы значительный объем статьи; полностью ты найдешь его только
на нашем диске.
Здесь первым делом мы объявляем и инициализируем несколько переменных. Затем в цикле проверяем корректность введенного ключа, а поскольку передается он в функцию в виде строки, то должен содержать лишь цифры от 0 до 9 и латинские буквы от A до F. Кроме этого, дополнительно мы проверяем длину ключа. После этого открываем входной и выходной файлы и проводим предварительную инициализацию нужных нам структур. Далее в листинге должен идти код шифрования, но, как мы уже говорили, он опущен. Вот и все, главное - после шифрации не забыть корректно закрыть файлы, чтобы сохранить все изменения.
Дешифрация из файла в память
Функцию дешифрации из файла в память мы, к сожалению, тоже не можем привести полностью, и в листинге опять будет опущена алгоритмическая часть. Прототип дешифрации выглядит так:
int DecryptToMemory(std::string fileName, std::string secretKey, unsigned char* &buffer)
Содержимое файла fileName расшифровывается ключом secretKey и помещается в участок памяти, на который ссылается переменная buffer. Код этой функции практически идентичен коду в листинге 3, но есть и небольшие отличия. Во-первых, мы открываем лишь один файл, а во-вторых, мы определяем размер файла и автоматически выделяем требуемый объем памяти для расшифрованной информации.
Последнее, что нам остается сделать, – это реализовать функцию расшифровки одного файла в другой. Ее прототип выглядит так:
int DecryptToFile(std::string inFileName, std::string outFileName, std::string key)
Содержимое inFileName расшифровывается ключом key и записывается в файл outFileName. Полный код этой функции можно увидеть в листинге.
Для начала мы объявляем указатель на unsigned char. Затем с помощью реализованной выше функции DecryptToMemory расшифровываем содержимое файла в память. Если все прошло успешно, то записываем участок памяти, на который ссылается переменная buffer, в файл outFileName и удаляем память, выделенную функцей
DecryptToMemory.
Проверка работы
И в заключение напишем маленькую программку, которая будет шифровать/дешифровать файлы. Для этого нам понадобится консольный проект (я использовал Visual Studio 2003). Включим в проект файлы нашего недавно созданного класса и то, что мы скачали из интернета (aes.h, aeskey.c, aescrypt.c, aesopt.h, aestab.c, aestab.h). Проект мы назвали AES_demo, а потому и главный cpp-файл называется aes_demo.cpp. В нем мы подключим caes.h и определим 256-битный ключ шифрования.
#include "caes.h" #define RESCRYPTO_KEY "1fe2daD67821f83092bbceeadf821fe0923ed923edfe98df98acD6238119faed"
Чтобы зашифровать какой-либо файл, надо запустить программу со следующими параметрами:
AES_demo.exe /encrypt not_crypt_file.txt crypt_file.txt
После такого вызова в crypt_file.txt мы увидим зашифрованное содержимое not_crypt_file.txt. Для дешифровки надо вбить в командную строку следующее:
AES_demo.exe /decrypt crypt_file.txt decrypt_file.txt
Если ты посмотришь на исходный код функции main, то увидишь, что мы просто проверяем количество аргументов, передаваемых программе, и затем вызываем соответствующие функции. Теперь ты можешь попробовать собрать проект (правда, в готовом виде он уже есть на
диске). После того как проект успешно скомпилирован, можно приступать непосредственно к тестированию. Если ты все сделал правильно, то после шифровки и расшифровки файла его содержимое должно остаться без изменений.
Вот и все. Конечно, для сдачи лабы одной программы недостаточно - надо уметь еще и объяснить, что ты написал. Но если бы мы начали здесь объяснять
алгоритм AES, на это ушел бы весь объем журнала. Советуем поискать тебе инфу в интернете, благо там ее предостаточно. Например, вот эта ссылка кратко описывает основные моменты, правда, на английском языке:
http://en.wikipedia.org/wiki/Advanced_Encryption_Standard. В общем, удачи, не прогуливай пары ;).