Основные криптографические алгоритмы на языке Си
9 июля 2018
Народная мудрость гласит, что правильно сделать шифрование в своем приложении крайне непросто. Свой велосипед почти наверняка будет содержать крайне неочевидные простому смертному дефекты, которые последние 20 лет исправлялись в существующих криптографических пакетах. Поэтому в любой непонятной ситуации нужно использовать готовые наработки, такие, как OpenSSL, LibreSSL, GPG или OTR. Но что делать, если для вашей конкретной задачи нет готового решения? Например, вы реализуете шифрование на уровне страниц для вашей СУБД, или вам нужно шифровать короткие сообщения, передаваемые с помощью NRF24L01 в самопальном IoT-проекте. В данном случае у вас действительно может не быть большого выбора. Но, по крайней мере, вы можете уменьшить шанс появления существенных дефектов в вашем приложении, используя проверенные временем алгоритмы и режимы шифрования.
Примечание: Подозреваю, что постоянные читатели данного блога не найдут в посте особого срыва покровов. Статья предназначена для начинающих специалистов, ну или же людей, ранее не имевших дела с криптографией. Также отмечу, что я не являюсь экспертом в криптографии, и моему мнению по вопросам из этой области не следует слишком сильно доверять.
Выбираем алгоритм хэширования
Некогда большой популярностью пользовались алгоритмы MD5 и SHA1. Но на момент написания этих строк они потеряли былую актуальность. Какие-то системы могут все еще использовать их в силу исторических причин. Однако в новом коде следует использовать более сильные алгоритмы, такие, как SHA-2 или SHA-3. Так, неплохим выбором, на практике используемым во многих современных системах, будет алгоритм SHA256, относящийся к семейству SHA-2. Ниже приведет пример хэширования блока данных при помощи этого алгоритма:
SHA256_Init(&ctx);
uint8_t res[SHA256_DIGEST_LENGTH];
char strres[SHA256_DIGEST_STRING_LENGTH];
while(/* ... тут условие ... */) {
// здесь читаем очередной блок данных в buff, например из файла
// ....
SHA256_Update(&ctx, (uint8_t*)buff, buff_size);
}
SHA256_Final(res, &ctx);
bytesToHex(res, SHA256_DIGEST_LENGTH, strres);
printf("SHA256 = %s\n", strres);
В полной версии исходников к этому посту вы найдете реализацию не только SHA256, но и SHA384, SHA512, а также MD5 и SHA1. Все алгоритмы в данной статье были позаимствованы из исходников PostgreSQL, что хоть как-то обеспечивает их проверенность временем. Также, благодаря лицензии BSD, их можно свободно использовать как в открытых, так и закрытых проектах. Все реализации покрыты автоматическими тестами, использующие соответствующие тестовые вектора.
Выбираем алгоритм шифрования и его режим работы
Такие алгоритмы, как IDEA, Blowfish, и уж тем более DES/3DES относятся к старому поколению алгоритмов, и их не следует использовать в современных системах. Также не является надежным некогда популярный алгоритм потокового шифрования RC4. К современным и надежным шифрам можно отнести AES (он же Rijndael, читается «рэндал») и Twofish. Первый использовать намного предпочтительнее, так как, будучи стандартом, он используется практически везде и изучен экспертами вдоль и поперек.
Но одного алгоритма шифрования недостаточно, так как блочные шифры могут использоваться в различных режимах. Их много, каждый со своими сильными и слабыми сторонами. Никогда не используйте режим электронной кодовой книги (ECB). Википедия наглядно объясняет, почему:
В качестве неплохого выбора можно назвать режим сцепления блоков шифротекста (CBC), а также решим счетчика (CTR), фактически превращающий блочный шифр в потоковый:
Пример использования алгоритма AES в режиме CBC:
test_cbc()
{
rijndael_ctx ctx;
bool equal;
uint8_t key[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15 };
uint8_t iv[] = { 15, 14, 13, 12, 11, 10, 9, 8,
7, 6, 5, 4, 3, 2, 1, 0 };
uint8_t data[16*3];
assert(sizeof(key) == 16);
assert(sizeof(iv) == 16);
assert((sizeof(data) % 16) == 0);
for(uint8_t i = 0; i < sizeof(data); i++)
data[i] = i;
aes_set_key(&ctx, key, sizeof(key)*8, 0);
aes_cbc_encrypt(&ctx, iv, data, sizeof(data));
equal = true;
for(uint8_t i = 0; i < sizeof(data); i++)
equal &= (data[i] == i);
assert(!equal);
aes_cbc_decrypt(&ctx, iv, data, sizeof(data));
for(uint8_t i = 0; i < sizeof(data); i++)
assert(data[i] == i);
printf("CBC test passed!\n");
}
Заметьте, что режиму CBC помимо ключа шифрования и шифруемых данных также нужен Initialization Vector (IV). Для надежного шифрования он должен быть уникальным для каждого шифруемого блока данных, в смысле никогда не повторяться. Например, если вы реализуете страничное шифрование для своей файловой системы или СУБД, то не можете просто использовать в качестве IV уникальный id страницы. Ведь в этом случае IV будет использован повторно при перезаписи страницы. Просто как пример только одного из неочевидных простым смертным дефекта. Возможное решение заключается в том, чтобы использовать неизменяемые страницы, например, хранить все в LSM-tree.
Про выбор всего остального
Криптография не ограничивается одними только блочными шифрами и алгоритмами хэширования. Для генерации ключей шифрования нужны криптографически стойкие псевдослучайные данные. Для решения этой проблемы существуют такие алгоритмы, как Fotuna и его улучшенная версия Yarrow. Для обмена ключами и подписи сообщений используется ассиметричная криптография. Здесь в дело вступают RSA, алгоритм Диффи-Хеллмана и эллиптические кривые.
Наконец, даже имея на руках все эти примитивы, их нужно уметь правильно использовать вместе. Наилучшим известным мне источником информации по этой теме является книга Брюса Шнайера и Нильса Фергюсона «Практическая криптография». Краткий пересказ основных моментов из книги вы найдете в заметке Памятка по созданию безопасного канала связи.
Пожалуй, это все, чем я хотел сегодня поделиться. Полная версия исходников к этому посту лежит в этом репозитории на GitHub. Вопросы и дополнения, как обычно, всячески приветствуются.
Метки: C/C++, Алгоритмы, Безопасность, Криптография.
Вы можете прислать свой комментарий мне на почту, или воспользоваться комментариями в Telegram-группе.