[Из песочницы] Алгоритм шифрования RC5 и его реализация на python

6baa474684cf46b9a5cdb8df9256d1ff.png

Алгоритм RC5


В своём посте, я хотел бы рассказать о симметричном алгоритме шифрования RC5 и моей версии его реализации на python. Данный алгоритм разработан известнейшим криптологом Рональдом Макдональдом Ривестом — одним из разработчиков системы RSA и основателей одноименной фирмы. По количеству пользователей RC5 стоит в одном ряду с такими известными алгоритмами как IDEA и Blowfish. Аббревиатура RC обозначает, по разным источникам, либо Rivest Cipher, либо Ron's Code, что в совокупности даёт нам «шифр Рона Ривеста». Заинтересовавшихся прошу под кат.
Введение
При описании алгоритма будем использовать следующие обозначения:

  • Размер слова 7c126b714976467f91143fd8cd0896e1.gifв битах. RC5 шифрует блоками по два слова; допустимыми значениями являются 16, 32 и 64. Данную величину рекомендуется брать равной машинному слову. Например для 32-битных машин 7c126b714976467f91143fd8cd0896e1.gif = 32 и следовательно размер блока будет равен 64 бита
  • Количество раундов алгоритма db62af3df14d400d9a6fcb90aba772ec.gif — целое число от 0 до 255 включительно. При значении 0 шифрование выполняться не будет
  • Размер секретного ключа 68967adb55484926a0baa4235f81008b.gifв байтах — целое число от 0 до 255 включительно


Для уточнения параметров, используемых в конкретном случае применяется обозначение RC5-dd4f155a07ab41e7897c736673315d4b.gif;
например, RC5-32/12/16 обозначает алгоритм RC5 c 64-битным блоком, 12 раундами шифрования и 16-байтным ключом(данная комбинация рекомендуется Ривестом в качестве основного варианта).

Работа алгоритма состоит из двух этапов:

  • Процедура расширения ключа
  • Само шифрование


Создадим класс и конструктор инициализирующий необходимые стартовые переменные

Python listing
class RC5:
    def __init__(self, W, R, key):
        self.W = W
        self.R = R
        self.key = key



Процедура расширения ключа
Предлагаю начать с этапа, который немного сложней, а именно, с процедуры расширения ключа. Для этого нам понадобится написать 3 простеньких функции:

  • Выравнивания ключа
  • Инициализации массива расширенных ключей
  • Перемешивания массивов ключей


Выравнивание ключа
Если размер ключа(в байтах) не кратен 841f898209ed4e65982fa3507c3d61b4.gif, дополняем его нулевыми байтами до ближайшего размера кратного 841f898209ed4e65982fa3507c3d61b4.gif. После этого ключ копируется в массив 3d2faeeae4234504a669f0e335f87a0a.gif, где 2970ce6daa7a46839defe962f142c1ee.gif. Проще говоря, мы копируем ключ блоками по 841f898209ed4e65982fa3507c3d61b4.gif байт (2, 4, 8 для значений 7c126b714976467f91143fd8cd0896e1.gif 16, 32, 64 соответственно) в массив 95dfc12ab992442d8e0baa9b59dbb76a.gif.
Например, при параметрах 59aae540e7d14fb88a64e2ec6527e91e.gif и значении ключа 97727206a58c42b39aab7b1af18eb75b.gif мы получим 45c8f0f7688347c0b903e4077dffa2a9.gif и 62b7c544cee84108952cda1d41c449ef.gif (под 0 подразумевается нулевой байт).

Опишем необходимую функцию

Python listing
def __keyAlign(self):
    while len(self.key) % (self.W // 8): # дополняем ключ байтами \x00
        self.key += b'\x00'
    self.c = len(self.key) // (self.W // 8)
    L, key = [], bin(int.from_bytes(self.key, byteorder='little'))[2:] # функция from_bytes преобразует массив байт в число
    for i in range(self.c): # Заполняем массив L
        L.append(int(key[:self.W], 2))
        key = key[self.W:]
    self.L = L



Инициализация массива расширенных ключей
На этом шаге нам нужно сгенерировать псевдослучайные константы 22172929927c44e78492510919530986.gif и 87d7bd4478a14e95b4018b58f8cbde70.gif по следующим формулам:
401dffab738f4136ac0d7e8f754c5cb4.gif
12edaf22966f4423934930647dab1197.gif,
где
c59d36b94cee44c6bee5d4cac3dc7bbf.gif — функция округления до ближайшего нечентного
ce99c0021628462caec8c946ede58d23.gif — экспонента
2bec1622a29549f7848bd142880bf544.gif — золотое сечение

Так же в спецификации алгоритма приведены уже вычисленные константы для всех возможных значений 7c126b714976467f91143fd8cd0896e1.gif:
c8d5716915f243f2835f285b7d66e779.gif
5465757ca0074b39b3c28c4e1b5fb4ce.gif
18a0edb205c049dc981bdbed9504298d.gif
c9c5ac7130734277b6d2b1b95d23b6b6.gif
b7c4d83ed96049ba9bbbc4f260808593.gif
4bfd253c201047af81fc02579b8f360a.gif,
все константы представлены в шестнадцатеричном виде.

Получив всё необходимое мы инициализируем массив 14c0318409954c5299dfb5d47f23963f.gif, где
412214c3bc3346a2a1a93a62eb4257ed.gif
8bab2a76ef2c417f8f640af2c57e0180.gif

Описание функций

Python listing
    def __const(self): # функция генерации констант
        if self.W == 16:
            return (0xB7E1, 0x9E37) # Возвращает значения P и Q соответсвенно
        elif self.W == 32:
            return (0xB7E15163, 0x9E3779B9)
        elif self.W == 64:
            return (0xB7E151628AED2A6B, 0x9E3779B97F4A7C15)


    def __keyExtend(self): # заполнение массива S
        P, Q = self.__const()
        self.S = [(P + i * Q) % 2 ** self.W for i in range(2 * self.R + 1)]



Перемешивание
Теперь, перед тем как приступить к шифрованию, нам осталось лишь перемешать элементы массивов L и S выполнив следующий цикл:
2c36792abd0643d0993a70bbbecba30c.gif
5eb1101f6b844bd689f89e7b59b6796d.gif
d5c8027d5b354704b4ae8a96a409bfac.gif
7c3ee2617e9d4618908d2b489af696f7.gif, где
ee8f29e843cf42afb91e4c5a56b5a497.gif — временные переменные, начальные значения равны 0
910e20582dbf4209948fc6736599cb3c.gif — массивы полученные на предыдущих шагах
Количество итераций 06ed7af42295476eaa99fad335d5ed77.gif определяется как a8873571264b43b4b2626caca9b6dd5d.gif

Python listing
def __shuffle(self):
    m = max(self.c, 2 * self.R + 1)
    i, j, A, B = 0, 0, 0, 0
    for k in range(3 * m):
        A = self.S[i] = self.__lshift((self.S[i] + A + B), 3)
        B = self.L[j] = self.__lshift((self.L[j] + A + B), A + B)
        i = (i + 1) % (2 * self.R + 1)
        j = (j + 1) % self.c


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

Структура алгоритма
Шифрование
Алгоритм представляет собой сеть Фейстеля, в каждом раунде которой(за исключением нулевого) выполняются следующие операции:
06212ecda1674f018670146136481463.gif
ae26e6f3304b4f8dbe25893894c14473.gif,
где
030a6791f03a498ea4e3a868477e2147 — номер текущего раунда, начиная с 1
8f9c29383c5a48c58346a52f3f20b533.gif — фрагмент расширенного ключа
2138f7ed1bdd46eeb7393549f5661cd7.gif — операция циклического сдвига на 805dd553f8a541069d6eb5c511a49c25.gif битов влево

В нулевом раунде выполняется операции наложения двух первых фрагментов расширенного ключа на шифруемые данные:
3ef39f3d0e98473da28752fd8850aa44.gif
57f7b1a8f3e8495ea9de91b0467b94ff.gif

Стоит отметить, что под раундом подразумевается преобразования, соответствующее двум раундам обычных алгоритмов, сконструированных на основе сетей Фейстеля. За раунд RC5 обрабатывает блок целиком, в отличии от раунда сети Фейстеля обрабатывающего один подблок — чаще всего половину блока.

Соответствующий код:

Python listing
def encrypt(self, inpFileName, outFileName): # в качестве параметров передаётся имя файла и открытым текстом и имя выходного файла
    with open(inpFileName, 'rb') as inp, open(outFileName, 'wb') as out: # получаем объекты файлов для чтения и записи
        while True:
            text = inp.read(self.W // 4) # считываем необходимое кол-во байт
            if not text: # выходим из цикла, если считали пустую строку(массив байт) 
                break
            text = text.ljust(self.W // 4, b'\x00') # последняя считанная строка может быть меньше необходимого размера, что критичного для блочного шифра, поэтому мы дополняем её нулевыми байтами
            A = int.from_bytes(text[:self.W // 8], byteorder='little')
            B = int.from_bytes(text[self.W // 8:], byteorder='little')
            A = (A + self.S[0]) % 2 ** self.W # нулевой раунд
            B = (B + self.S[1]) % 2 ** self.W
            for i in range(1, self.R): 
                A = (self.__lshift((A ^ B), B)
                     + self.S[2 * i]) % 2 ** self.W
                B = (self.__lshift((A ^ B), A)
                     + self.S[2 * i + 1]) % 2 ** self.W
            out.write(A.to_bytes(self.W // 8, byteorder='little') +
                     B.to_bytes(self.W // 8, byteorder='little')) # запись зашифрованных данных в выходной файл



Расшифровывание
Расшифровка данных выполняется применением обратных операций в обратной последовательности, т.е. сначала выполняем следующий цикл:
255df6edbdd24c7ab507969c618719d2.gif
e50a479e53de4dffaaefaa941f9f629f.gif,
где
951d3f197a2f4ab993d02776d74edde6.gif — операция циклического сдвига вправо
030a6791f03a498ea4e3a868477e2147 — номер раунда в обратном порядке, т.е. начиная с db62af3df14d400d9a6fcb90aba772ec.gif и заканчивая единицей.

После этого выполняются операции обратные для нулевого раунда, а именно:
b5761dab08b54b67b8e1cad5c2432b4c.gif
2b27559df7cf4e839ab3200800381b59.gif

Код тут:

Python listing
def decrypt(self, inpFileName, outFileName):
    with open(inpFileName, 'rb') as inp, open(outFileName, 'wb') as out:
        while True:
            text = inp.read(self.W // 4)
            if not text:
                break
            A = int.from_bytes(text[:self.W // 8], byteorder='little')
            B = int.from_bytes(text[self.W // 8:], byteorder='little')
            for i in range(self.R - 1, 0, -1):
                B = self.__rshift(
                    ((B - self.S[2 * i + 1]) % 2 ** self.W), A) ^ A
                A = self.__rshift(
                    ((A - self.S[2 * i]) % 2 ** self.W), B) ^ B
            B = (B - self.S[1]) % 2 ** self.W
            A = (A - self.S[0]) % 2 ** self.W
            res = (A.to_bytes(self.W // 8, byteorder='little')
                   + B.to_bytes(self.W // 8, byteorder='little'))
            out.write(res.rstrip(b'\x00')) # убираем добавленные на этапе шифрования нулевые байты



Алгоритм поразительно прост — в нем используются только операции сложения по модулю 2 и по модулю 78848d443f704e81b1a2b1e2a402aec7.gif, а также сдвиги на переменное число битов. Последняя из операций представляется автором алгоритма как революционное решение, не использованное в более ранних алгоритмах шифрования (до алгоритма RC5 такие использовались только в алгоритме Madryga, не получившем широкого распространения), — сдвиг на переменное число битов является весьма просто реализуемой операцией, которая, однако, существенно усложняет дифференциальный и линейный криптоанализ алгоритма. Простота алгоритма может рассматриваться как его важное достоинство — простой алгоритм легче реализовать и легче анализировать на предмет возможных уязвимостей.

Код целиком можно посмотреть на github.

Немного криптоанализа

  • Существует класс ключей при использовании которых алгоритм можно вскрыть линейным криптоанализом. В других случаях это почти невозможно.
  • Дифференциальный криптоанализ более эффективен при атаке на данный алгоритм. Например, для алгорима RC5-32-12-16, в лучшем случае, требуется 400ce48adc824d56a0f2134c94e0e3ff.gif выбранных открытых текстов для успешной атаки. При использовании 18-20(и больше) раундов вместо 12 вскрыть алгоритм с помощью дифференциального криптоанализа почти невозможно.


Таким образом, наиболее реальным методом взлома алгоритма RC5 (не считая варианты с небольшим количеством раундов и с коротким ключом) является полный перебор возможных вариантов ключа шифрования. Что означает, что у алгоритма RC5 практически отсутствуют недостатки с точки зрения его стойкости. На этом и хотелось бы закончить. Всем спасибо за внимание.

© Habrahabr.ru