Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #99, МАРТ 2007 г.

Х-Лаба #2

Deeoni$

Хакер, номер #099, стр. 126


(DeeoniS@gmail.com, ICQ: 982-622)

Реализация криптографического алгоритма AES (Advanced Encryption Standard)

Сегодня в нашей недавно открытой подрубрике мы постараемся решить очередную лабораторную работу, которую прислал нам некий Николай П. из города на Неве. Итак, приступим.

Тема: Реализация криптографического алгоритма AES (Advanced Encryption Standard)

Задание: разработать библиотеку классов, реализующих криптографический алгоритм 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 является одним из самых распространенных алгоритмов симметричного шифрования в мире.

История

В далеком 1998 году NIST объявил конкурс на создание алгоритма, удовлетворяющего выдвинутым институтом требованиям. Он опубликовал все несекретные данные о тестировании кандидатов на роль AES и потребовал от авторов алгоритмов сообщить о базовых принципах построения используемых в них констант. В отличие от ситуации с DES, NIST при выборе AES не стал опираться на секретные и, как следствие, запрещенные к публикации данные об исследовании алгоритмов-кандидатов.

Чтобы быть утвержденным в качестве стандарта, алгоритм должен был:

1)реализовать шифрование частным ключом;

2)представлять собой блочный шифр;

3)работать со 128-разрядными блоками данных и ключами трех размеров (128, 192 и 256 разрядов).

Дополнительно кандидатам рекомендовалось:

1)использовать операции, легко реализуемые как аппаратно (в микрочипах), так и программно (на персональных компьютерах и серверах);

2)ориентироваться на 32-разрядные процессоры;

3)не усложнять без необходимости структуру шифра для того, чтобы все заинтересованные стороны были в состоянии самостоятельно провести независимый криптоанализ алгоритма и убедиться, что в нем не заложено каких-либо недокументированных возможностей.

Кроме того, алгоритм, претендующий на роль стандарта, должен распространяться по всему миру на неэксклюзивных условиях и без платы за пользование патентом.

Перед первым туром конкурса в NIST поступило 21 предложение, 15 из которых соответствовали выдвинутым критериям. Затем были проведены исследования этих решений, в том числе связанные с дешифровкой и проверкой производительности, и получены экспертные оценки специалистов по криптографии. В августе 1999 года NIST объявил пять финалистов, которые получили право на участие во втором этапе обсуждений.

2 октября 2000 года NIST сообщил о своем выборе – победителем конкурса стал алгоритм RIJNDAEL (произносится как «райндол») бельгийских криптографов Винсента Раймана и Йоана Дамана, который зарегистрирован в качестве официального федерального стандарта как FIPS 197 (Federal Information Processing Standard).

Для меня остается загадкой, зачем в российском вузе преподают стандарты иностранных государств. Видимо, исходят из принципа, что врага надо знать в лицо :). Ладно, в общем-то, это не наше дело. Нам надо просто программно реализовать основу национальной безопасности США.

Начало работы

Итак, приступим. Так как по заданию нам надо было разработать библиотеку классов, то начнем с описания этих классов, то есть с заголовочных файлов. Наш класс будет иметь три метода, которые реализуют основные операции, перечисленные в задании: encryptFile - шифрация данных из файла в файл, DecryptToFile - дешифрация данных из файла в файл, DecryptToMemory - дешифрация данных из файла в память. Собственно, в заголовочном файле нет ничего особенного, поэтому просто приведу его листинг с комментариями (смотри листинг 1).

Как видно из листинга, мы определяем всего три вышеперечисленных метода и конструктор с деструктором. Теперь займемся реализацией этих методов.

Вспомогательный код

Прежде чем приступить непосредственно к выполнению трех основных пунктов задания, надо подготовиться, то есть написать пару вспомогательных функций и подключить несколько заголовочных файлов. Эти файлы мы нашли в интернете, ведь ты не думал, что мы будем с нуля реализовывать криптографический госстандарт США. В этих файлах содержится куча полезных структур и алгоритмов, которые позволяют заниматься разработкой непосредственно класса, а не вникать в сложную математику AES. В большинстве случаев преподаватели сами предоставляют подобные куски кода, чтобы облегчить и без того тяжелую жизнь студента. Все эти файлы ты найдешь (а при желании и детально ознакомишься с их содержанием) на нашем DVD.

Взглянем на листинг 2. Там как раз описаны две наши вспомогательные функции и один макрос.

Все это хозяйство служит для заполнения массива псевдослучайных чисел. Непосредственно же заполнением занимается эта функция:

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. Код этой функции частично представлен в листинге 3.

Весь код, к сожалению, мы привести не можем, поскольку он занял бы значительный объем статьи; полностью ты найдешь его только на нашем диске.

Здесь первым делом мы объявляем и инициализируем несколько переменных. Затем в цикле проверяем корректность введенного ключа, а поскольку передается он в функцию в виде строки, то должен содержать лишь цифры от 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. Полный код этой функции можно увидеть в листинге 4.

Для начала мы объявляем указатель на 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. В общем, удачи, не прогуливай пары ;).

#pragma once

#include <string>

#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);

};

// Для шифрования

void cycles(volatile unsigned __int64 *rtn)

{

#if defined(_MSC_VER)

__asm // считываем Pentium Time Stamp Counter

{ cpuid

rdtsc

mov ecx,rtn

mov [ecx],eax

mov [ecx+4],edx

cpuid

}

#else

#include <time.h>

time_t tt;

tt = time(NULL);

((unsigned long*)rtn)[0] = tt;

((unsigned long*)rtn)[1] = tt & -36969l;

return;

#endif

}

#define RAND(a,b) (((a = 36969 * (a & 65535) + (a >> 16)) << 16) + \

(b = 18000 * (b & 65535) + (b >> 16)))

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++];

}

}

int CAes::encryptFile(std::string inFileName, std::string outFileName, std::string secretKey)

{

gen_tabs();

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;

}

int CAes::DecryptToFile(std::string inFileName, std::string outFileName, std::string key)

{

unsigned char* buffer;

int decrypted = DecryptToMemory(inFileName, key, buffer);

if (decrypted > 0)

{

FILE* outFile = fopen(outFileName.c_str(), "wb");

if (outFile)

{

fwrite(buffer, sizeof(unsigned char), decrypted, outFile);

fclose(outFile);

}

delete buffer;

}

return decrypted;

}

Содержание