Алгоритм шифрования Anubis на PHP

в 11:11, , рубрики: php, криптография, шифрование на php, метки: , ,

Anubis
Продолжая своеобразную неделю криптографии на Хабре, я решил поделиться своей реализацией алгоритма шифрования Anubis на PHP. Anubis представляет собой блочный алгоритм шифрования, являющийся, по-сути, модификацией алгоритма Rijndael, принятого в качестве стандарта шифрования в США. Авторами шифра являются Винсент Рэймен — один из разработчиков Rijndael и Пауло С. Л. М. Баррето — известный криптограф, один из разработчиков хэш-функции Whirlpool.

Почему я выбрал именно Anubis? Это не патентованный алгоритм, доступный для свободного использования. Anubis отвечает современным требованиям безопасности — размер блока составляет, как и в AES, 128 бит, а длина ключа может варьироваться от 128 до 320 бит. Кроме того, с момента опубликования в 2000-м году, в алгоритме Anubis не обнаружено слабых мест. Он не попал в проект NESSIE, но лишь из-за своей схожести с Rijndael.

Внизу есть ссылка на официальную страницу алгоритма, где интересующиеся смогут найти его полное описание, а также примеры реализации на C и Java. Для своей реализации я использовал модифицированную версию алгоритма («tweaked» Anubis), которая отличается тем, что использует не псевдослучайный S-Box, а подобранный авторами оптимальный. В результате у меня получился класс со следующим интерфейсом:

class Anubis {
    /* Properties */
    string $key             //собственно, ключ
    string $KDF_salt        //соль для функции вывода ключа
    string $KDF_algo        //хэш-алгоритм для функции вывода ключа
    int    $file_blocksize  //размер блока для файловых операций

    /* Methods */
    string function encrypt($data) //шифрование строки
    string function decrypt($data) //расшифровка строки

    void   function encryptFile($src, $dest) //шифрование файла
    void   function decryptFile($src, $dest) //расшифровка файла

    void   function setKey($key, $raw_key = false) //установка ключа
}

Метод Anubis::setKey($key, $raw_key = false), как ясно из его названия, предназначен для установки ключа. В своей реализации я предусмотрел как возможность использования простого текстового ключа (например, "VeryStrongPassword"), так и возможность указания битовой строки (пример: hex2bin('575a42654a85020b4f6eaeff03aecb0e')). Для использования битовой строки в качестве ключа и предназначен параметр $raw_key.

Если для ключа использовать битовую строку, то мы контролируем содержание каждого бита ключа и его длину (от 128 до 320 бит с шагом в 32 бита), но и забота о его корректности ложится на нас. В частности, если ключ будет слишком длинным, слишком коротким, или не кратным 32 битам, метод Anubis::setKey() выбросит исключение. Если же мы используем простой текстовый ключ, то перед шифрованием он будет преобразован функцией вывода ключа (в литературе также можно встретить такие переводы, как «функция генерирования ключа» или «функция перемалывания ключа»).

В качестве функции вывода ключа я использовал обычный HMAC, параметры которого задаются соответствующими свойствами (Anubis::KDF_salt, Anubis::KDF_algo). По-умолчанию, в качестве соли используется строка из 16 нулевых октетов "", а в качестве алгоритма — SHA-256. В принципе, можно было бы использовать обычный MD5 вообще без всякой соли, ведь при выводе ключа нам нужно лишь разрушить ограничения исходного пароля, такие как неподходящая длина (менее 128, более 320 или не кратен 32 битам), ограниченный набор символов (обычно, a-zA-Z0-9!@#$%^&*(){}[]/*-+.,?';:~`_"|) и статистические зависимости текста (если пароль представляет собой текстовую фразу), но я решил все же оставить больше простора для настройки алгоритма под конкретные задачи.

Свойство Anubis::key также предназначено для установки ключа, но возможность указания битовой строки отсутствует. Лично мне всегда больше нравится использовать именно свойства (заметьте, не поля, являющиеся переменными объекта, доступными извне, а именно свойства — когда каждая операция чтения или записи предусматривает вызов соответствующих методов), по-моему, так удобнее и лаконичнее.

Еще немного об использовании текстовых ключей. Как я уже сказал, ключ в шифре Anubis должен быть от 128 бит (16 Байт) до 320 бит (40 Байт) с шагом 32 бита (4 Байта). Текстовый ключ, назначенный, например, через Anubis::key, очень часто не будет подходить под указанные размеры. В принципе, с помощью функции вывода ключа, можно было бы пойти путем наименьшего сопротивления и просто уравнять все ключи в размере — либо сделать их минимальными, либо включить режим параноика и наоборот, максимизировать длину до 320 бит. Но я посчитал такой подход неправильным, ведь если я могу устанавливать ключи разной длины, соответственно я вправе ожидать, что и система шифрования будет использовать ключи разной длины.

В итоге, в моей реализации, ключ, после обработки функцией вывода ключа, всегда будет иметь минимально-возможную длину, но не менее длины исходного ключа. Т.е. если я, например, введу ключ "010101010101" (12 байт), то при шифровании ключ будет расширен до 16 байт (128 бит). Если же я введу "10101010101010101" (17 байт), то используемый при шифровании ключ будет длиной уже 20 байт (160 бит), и так далее.

Про ключ сказано уже достаточно, так что пора вернуться к описанию других методов.

Названия методов Anubis::encrypt($data) и Anubis::decrypt($data) говорят сами за себя. Понятно, что decrypt(encrypt($data)) == $data.

Тут необходимо пояснить, каким образом выполняется шифрование сообщения. Сам по себе, Anubis является блочным шифром, и способен шифровать, как ясно из названия, блоки. Т.е. исходное сообщение разбивается на блоки, в нашем случае длиной 128 бит, и каждый блок шифруется ключом отдельно. Я думаю понятно, что шифровать таким способом сообщения длиной более одного блока (а у нас, напомню, это всего 16 байт) нельзя — сохраняются некоторые статистические зависимости, так что зашифрованный текст можно анализировать, особенно, если он достаточно большой, либо, если сообщений, зашифрованных одним ключом, много. Проблема здесь в том, что один и тот же блок текста при использовании одного ключа, всегда будет представлен одним и тем же блоком зашифрованного сообщения.

Для преодоления описанных проблем, я использовал режим сцепления блоков шифротекста, при котором каждый следующий блок шифруется с применением информации, полученной при шифровании предыдущего. При шифровании первого блока, вместо информации от шифрования предыдущего, используется специальный вектор инициализации. Таким образом, при условии использования одноразовых векторов инициализации, один и тот же блок исходного текста каждый раз будет давать различный шифротекст, что лишает потенциального взломщика какой-либо статистической информации для анализа.

В моей реализации для генерирования вектора инициализации используется метка времени с точностью до микросекунд и псевдослучайное число (на случай, если каким-то образом получится одновременно создать два вектора инициализации для одного ключа).

После того, как мы сгенерировали и применили вектор инициализации, его надо как-то передать вместе с зашифрованным сообщением, иначе потом не получится корректно расшифровать наш текст. Так как вектор инициализации в тайне держать не нужно, в своей реализации я просто вставляю его первым блоком в зашифрованное сообщение. При расшифровке, из шифротекста сначала считывается вектор инициализации, а уже потом непосредственно зашифрованные данные.

Еще одна особенность, вытекающая из свойства блочных шифров — размер сообщения всегда должен быть кратен размеру блока. Если же это не так, то остаток сообщения должен дополняться до размера блока. Проблема здесь в том, что при расшифровке дополненные байты надо как-то удалять. Самым простым решением было бы дополнить остаток нулевыми символами, а при расшифровке их просто удалить. Но, во-первых, кто сказал, что в исходном сообщении не может быть нулевых символов, и мы просто напросто не удалим часть самого сообщения? А во-вторых, зачем давать потенциальному взломщику дополнительную информацию о том, что в конце сообщения часто находится от 1 до 15 нулевых символов?

Потому я пошел несколько другим путем. Остаток сообщения дополняется псевдослучайными байтами, а в последнем байте содержится информация о количестве добавленных символов. Если же вдруг длина исходного сообщения уже кратна размеру блока, и никакого остатка нет, то один полностью заполненный псевдослучайными символами блок приходится добавлять — ведь алгоритм заранее не может знать, добавлялись ли байты или нет, и все равно удалит в конце столько символов, сколько указано в последнем байте. Конечно, определенные зависимости здесь также остаются — последний байт исходного сообщения теперь всегда имеет значение от chr(1) до chr(16), всего 16 вариантов. Но это, все же, немного лучше, чем серия вполне определенных символов, особенно если исходное сообщение предварительно обработано для разрушения характеристик исходного текста, которые могут быть известны потенциальному взломщику (например, текстовое сообщение может быть сжато алгоритмом, не предусматривающим определенного «хвоста»).

Методы Anubis::encryptFile($src, $dest) и Anubis::decryptFile($src, $dest) аналогичны Anubis::encrypt($data) и Anubis::decrypt($data) за тем лишь исключением, что исходное сообщение здесь читается из файла с именем $src, а результат записывается в файл $dest. Данные читаются и пишутся чанками (здесь я избегаю слова «блок» дабы не пусть его с блоком, которыми производится шифрование данных), размер которых можно устанавливать через свойство Anubis::file_blocksize, подбирая его таким образом, чтобы в используемой файловой системе операции чтения и записи проводились как можно быстрее, затрачивая при этом разумное количество памяти (чанк читается в оперативную память целиком). По-умолчанию, размер чанка установлен в половину мегабайта.

Ну вот, собственно, и все описание. Осталось только привести небольшой пример использования:

<?php
require_once 'anubis.class.php';

$src  = 'secret_message.txt';
$encrypted = 'encrypted.file';
$decrypted = 'decrypted_message.txt';

$cypher = new Anubis();

$cypher->key = 'strong password';

$cypher->encryptFile($src, $encrypted);
$cypher->decryptFile($encrypted, $decrypted);

$src_hash       = md5_file($src);
$decrypted_hash = md5_file($decrypted);

echo "Src:  $src_hashn";
echo "Dest: $decrypted_hashn";

Напоследок скажу, что шифрование получилось не таким уж и быстрым. При максимальной длине ключа у меня система могла шифровать около 100 КБ/с (примерно 6400 блоков в секунду), при минимальной длине ключа — около 150-160 КБ/с (примерно 10000 блоков в секунду). Так что если у кого-нибудь есть предложения по оптимизации моей реализации алгоритма (профилировщик говорит о том, что все это время съедается именно приватным методом Anubis::crypt() — непосредственно функцией шифрования/расшифровки), буду очень раз выслушать.

Репозитории проекта на GitHub (исходный код, примеры, wiki): https://github.com/kolonist/php-anubis
Официальная страница алгоритма Anubis: http://www.larc.usp.br/~pbarreto/AnubisPage.html

Автор: Kolonist

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js