Pull to refresh

Разработка собственного алгоритма симметричного шифрования на Php

Reading time6 min
Views5.6K

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


И, на данный момент из нее получилось то, что я назвал GenCoder.


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


Собственно, исходный код класса можно посмотреть здесь, а потестировать можно тут.


Итак, задача (данного исследовательского эксперимента, так его назовем) стояла следующая:


Разработать собственный алгоритм обратимого шифрования, притом что:


  • Каждый раз одно и то же сообщение будет зашифровываться уникальным образом и не повторяться.
  • Внедрить так называемую "рандомизацию" секретного ключа — найти способ как шифровать сообщения не постоянно одним и тем же секретным ключом, а некой секретной строкой, являющейся функцией от секретного ключа и исходного сообщения.
  • Как маловажное дополнение, сделать переменным по длине секретный ключ при с сохранением высокой криптостойкости алгоритма (в текущей версии — от 64 до 100 символов).
  • Как было сказано выше, не привязываться к существующим решениям, а сделать нечто свое, без сложной математики, с простым и понятным алгоритмом.

Поехали.


Для разработки было выбрано симметричное шифрование.


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


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


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


Основная идея прелесть получившегося алгоритма заключается в том, что для каждого акта шифрования не используется один и тот же секретный ключ, а генерируется функция обхода исходного ключа, так называемая сигнатура пути обхода (pathKeySignature в коде), причем функция зависит от нескольких аргументов. А именно — от сложной комбинации sha-512 хешей соли приложения, исходного сообщения, уникального идентификатора uniqid, а также от хешкодов отправителя и получателя сообщения.


Что такое сигнатура обхода ключа на пальцах? Если упростить, это фактически алгоритм обхода ключа. Например, берем первый символ ключа, затем 14-ый, затем 22-ой, затем 37-ой, затем 49-ый и т.д. Каждый новый такой обход — уникален (в силу уникальности генерируемого pathKeySignature).


Кроме того, в алгоритм внесены элементы двухфакторного шифрования (более подходящего термина не нашел, речь идет о коротких пин-паролях у отправителя и получателя, подробнее ниже по тексту). Далее, непосредственно "под капотом" используется обычный xor-метод.


"Двухфакторность" проявляется в том, что для генерации сигнатуры пути используются небольшие пароли (pass1 и pass2, по 4 символа), притом, что получателю неизвестен пароль отправителя, а отправителю неизвестен пароль получателя, но притом они обмениваются функциями-хешкодами от соли приложения и пароля второй стороны.


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


Предварительно магия начинается здесь.


private function attachKey($message, $salt)
    {
        return md5(hash('sha512', $message . uniqid() . $salt) . hash('sha512', $salt));
    }

    private function pathKeySignature($user_code_1, $user_code_2, $attach_key)
    {
        return hash('sha512', $user_code_1 . $attach_key . $user_code_2);
    }

Собственно, наличие md5 хеш-функции может поначалу немного смутить, но этот элемент алгоритма отвечает лишь только за внесение хаотичности, лавинности изменений (как и sha512) в генерируемый attachKey. Функция uniqid отдает нам уникальный идентификатор, что по итогу позволяет шифровать одно и то же исходное сообщение каждый раз новым шифром, что в конечном счете гуд. Зачем так заморачиваться? Представьте, что вы разрабатываете свой месседжер с шифрованием. Шифрование одних и тех же сообщений одним и тем же конечным шифром — если не прямая уязвимость, то явный шаг в ее сторону. Почему? Как правило, в данном контексте пользователи обмениваются весьма однотипным набором данных, из серии "привет!", "я понял", "ок", "скоро буду", "как дела?". Допустим, к примеру, у нас такой месседжер. Канал шифрования установлен, пользователь 1 отправил пользователю 2 шифрованное сообщение "0372985dee", пользователь 2 прочел сообщение и через 5 секунд ответил "0372985dee" обратно. Что зашифровано там? Ответ почти очевиден.
Это было лирическое отступление на тему уместности uniqid, возвращаемся к коду.


Магия продолжается здесь. Собственно, здесь в методе cipher происходит непосредственно обход ключа посредством сигнатуры $path_key_signature, а метод byteShifting дополнительно вносит хаотичность в генерируемый шифр посредством добавления различных сдвигов в определенных границах.


private function cipher($path_key_signature, $message, $generateKey)
    {
...
        for ($i = 0; $i < count($message); $i++) {
            if ($sign_key >= self::hash_length) $sign_key = 0;
            $key_code_pos = hexdec($path_key_signature[$sign_key]);
            $cur_key_pos = $cur_key_pos + $key_code_pos;
            if ($cur_key_pos >= $key_length) {
                $cur_key_pos = $cur_key_pos - $key_length;
            }
            $shifted_key_symbol = $generateKey[$cur_key_pos];
            // byte shifting
            $shifted_key_symbol = $this->byteShifting($i, $shifted_key_symbol);
            $shifter = $this->mb_ord($message{$i}) ^ $this->mb_ord($shifted_key_symbol);
            $cipher_message .= $this->mb_chr($shifter);
            $sign_key++;
        }
        return $cipher_message;
    }

Непосредственно в публичном методе шифрования не так много интересного, ибо все интересное а ля создание присоединяемого хеша attachKey и генерация пути обхода pathKeySignature описаны выше. Разве что, стоит отметить, что к выходящему наружу в незащищенный канал шифру конкатенируется $attach_key


public function codeMessage($message, $generateKey, $user1_pass, $receiver_hashcode)
    {
        $sender_hashcode = $this->sender_hashcode($user1_pass);
        $attach_key = $this->attachKey($message, $this->salt);
        $path_key_signature = $this->pathKeySignature($sender_hashcode, $receiver_hashcode, $attach_key);
        $result_cipher = $this->cipher($path_key_signature, $message, $generateKey) . $attach_key;
        $result_cipher = base64_encode($result_cipher);
        return gzencode($result_cipher, 9);
    }

Возможно, возникнет резонный вопрос. Почему бы не ограничиться в функции пути обхода просто одним attachKey, для чего добавлять туда хешкоды отправителя и получателя? Ответ находится тут же, "рядом" — attachKey расположен фактически на виду в создаваемом шифре, и зная его, не составляет труда реконструировать последовательность прохождения ключа при шифре. Сам секретный ключ это, разумеется, напрямую восстановить не даст, но сведет на нет всю идею "рандомизации" ключа. Поэтому, нужен дополнительный фактор, которым являются хешкоды получателей. Они представляют собой своего рода второй секретный элемент алгоритма помимо ключа. Этакое своего рода двухфакторное шифрование.


Метод decodeMessage детально рассматривать не будем, он практически тривиален, если учитывать полную симметричность с методом шифрования только в обратной последовательности.


Алгоритм выглядит вполне законченным, по крайней мере для некоего теоретического эксперимента.


Плюсы:


  • Относительно быстр по скорости шифрования/дешифрования (об этом ниже)
  • Прост и понятен
  • Открыт для изучения и улучшения, ибо опенсорс

Минусы:


  • (скорее обстоятельство, чем недостаток) Требуется детальный анализ, чтобы понимать насколько он криптографически стоек.

В двух словах по поводу тестирования.


По скорости работы алгоритма — относительно быстр (разумеется, все относительно и познается в сравнении, речь о скорости шифрования в рамках высокоуровнего яп вообще и в рамках php в частности). 2 мегабайта рандомного текста было зашифровано и расшифровано за 4 секунды, использовался при этом php 7.2. Система: Intel Core i7-8700 CPU @ 3.20GHz × 12, параллельно еще работал браузер с кучей вкладок и виртуальная машина. Резюме — скорость шифрования ~ 1 мб/сек на среднестатистическом железе на php7.0 и выше.

Tags:
Hubs:
Total votes 14: ↑3 and ↓11-4
Comments14

Articles