📜 ⬆️ ⬇️

RC5 encryption algorithm and its implementation in python


RC5 algorithm


In my post, I would like to talk about the RC5 symmetric encryption algorithm and my version of its implementation in python. This algorithm was developed by the most famous cryptologist Ronald Macdonald Rivest - one of the developers of the RSA system and the founders of the eponymous company. By the number of users, RC5 ranks with such well-known algorithms as IDEA and Blowfish. RC stands for, according to various sources, either Rivest Cipher or Ron's Code, which together gives us the “Ron Rivest cipher”. Interested please under the cat.

Introduction

In the description of the algorithm we will use the following notation:

To clarify the parameters used in a particular case, the notation RC5- is used. ;
for example, RC5-32 / 12/16 denotes the RC5 algorithm with a 64-bit block, 12 rounds of encryption and a 16-byte key (this combination is recommended by Rivest as the main option).

The work of the algorithm consists of two stages:

Create a class and constructor initializing the necessary starting variables.
Python listing
def __init__(self, w, R, key): self.w = w self.R = R self.key = key self.T = 2 * (R + 1) self.w4 = w // 4 self.w8 = w // 8 self.mod = 2 ** self.w self.mask = self.mod - 1 self.b = len(key) self.__keyAlign() self.__keyExtend() self.__shuffle() 


Key Expansion Procedure

I propose to start with a stage that is a little more complicated, namely, with the key expansion procedure. For this we need to write 3 simple functions:

Key alignment

If the key size (in bytes) is not a multiple , we supplement it with zero bytes to the nearest multiple . After that, the key is copied to the array. where . Simply put, we copy the key in blocks of bytes (2, 4, 8 for values 16, 32, 64 respectively) into an array .
For example, with parameters and key value we'll get and (0 means zero byte).

We describe the necessary function
Python listing
  def __keyAlign(self): if self.b == 0: #   self.c = 1 elif self.b % self.w8: #    w / 8 self.key += b'\x00' * (self.w8 - self.b % self.w8) #    \x00 self.b = len(self.key) self.c = self.b // self.w8 else: self.c = self.b // self.w8 L = [0] * self.c for i in range(self.b - 1, -1, -1): #   L L[i // self.w8] = (L[i // self.w8] << 8) + self.key[i] self.L = L 


Initialization of the extended key array

In this step, we need to generate pseudo-random constants. and according to the following formulas:

,
Where
- rounding function to the nearest inappropriate
- Euler number
- golden ratio
')
Also in the specification of the algorithm are already calculated constants for all possible values. :





,
all constants are represented in hexadecimal.

Having got everything we need, we initialize the array. where



Feature Description
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) % self.mod for i in range(self.T)] 


Stirring

Now, before proceeding to encryption, we need only mix the elements of the arrays L and S by executing the following cycle:



where
- temporary variables, initial values ​​are 0
- arrays obtained in the previous steps
Number of iterations defined as

Python listing
  def __shuffle(self): i, j, A, B = 0, 0, 0, 0 for k in range(3 * max(self.c, self.T)): 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) % self.T j = (j + 1) % self.c 

Lshift and rshift (which we will meet below) are logical left and right operations, respectively.
I think that their comments will be redundant, and the code can be viewed on github (link at the end)


Algorithm structure

Encryption

The algorithm is a network of its Feistel, in each round of which (with the exception of zero) the following operations are performed:

,
Where
- number of the current round, starting with 1
- extended key fragment
- cyclic shift operation on bits left

In the zero round, the operations of imposing the first two fragments of the extended key on the encrypted data are performed:



It is worth noting that a round means transformations, corresponding to two rounds of conventional algorithms designed on the basis of Feistel networks. For a round, RC5 processes the block as a whole, in contrast to the round of the Feistel network processing one sub-block - most often half of the block.

Corresponding code:
Python listing
  def encryptBlock(self, data): A = int.from_bytes(data[:self.w8], byteorder='little') B = int.from_bytes(data[self.w8:], byteorder='little') A = (A + self.S[0]) % self.mod B = (B + self.S[1]) % self.mod for i in range(1, self.R + 1): A = (self.__lshift((A ^ B), B) + self.S[2 * i]) % self.mod B = (self.__lshift((A ^ B), A) + self.S[2 * i + 1]) % self.mod return (A.to_bytes(self.w8, byteorder='little') def encryptFile(self, inpFileName, outFileName): #              with open(inpFileName, 'rb') as inp, open(outFileName, 'wb') as out: run = True while run: text = inp.read(self.w4) if not text: break if len(text) != self.w4: text = text.ljust(self.w4, b'\x00') #        ,     ,       run = False text = self.encryptBlock(text) out.write(text) 


Decryption

Data decryption is performed by applying the inverse operation in reverse order We first perform the following cycle:

,
Where
- cyclic right shift operation
- the round number in the reverse order, i.e. beginning with and ending with a unit.

After this, the operations are reverse for the zero round, namely:



Code here:
Python listing
  def decryptBlock(self, data): A = int.from_bytes(data[:self.w8], byteorder='little') B = int.from_bytes(data[self.w8:], byteorder='little') for i in range(self.R, 0, -1): B = self.__rshift(B - self.S[2 * i + 1], A) ^ A A = self.__rshift(A - self.S[2 * i], B) ^ B B = (B - self.S[1]) % self.mod A = (A - self.S[0]) % self.mod return (A.to_bytes(self.w8, byteorder='little') + B.to_bytes(self.w8, byteorder='little')) def decryptFile(self, inpFileName, outFileName): with open(inpFileName, 'rb') as inp, open(outFileName, 'wb') as out: run = True while run: text = inp.read(self.w4) if not text: break if len(text) != self.w4: run = False text = self.decryptBlock(text) if not run: text = text.rstrip(b'\x00') #      b'\x00' out.write(text) 


The algorithm is strikingly simple - it uses only modulo 2 and modulo addition operations. as well as shifts by a variable number of bits. The last of the operations is presented by the author of the algorithm as a revolutionary solution that was not used in earlier encryption algorithms (prior to the RC5 algorithm, these were used only in the Madryga algorithm, which was not widely used) - a shift by a variable number of bits is a very simple operation, but significantly complicates the differential and linear cryptanalysis of the algorithm. The simplicity of the algorithm can be considered as its important advantage - a simple algorithm is easier to implement and easier to analyze for possible vulnerabilities.

The whole code can be viewed on github .
Bit of cryptanalysis


Thus, the most realistic method of cracking the RC5 algorithm (not counting the options with a small number of rounds and with a short key) is a complete search through the possible options for the encryption key. Which means that the RC5 algorithm has practically no flaws in terms of its durability. On this and I would like to finish. Thank you all for your attention.

UPD: Changed operation lshift, rshift. Added constants to the class constructor. Added tests.
Thank you mas for advice and help.

Source: https://habr.com/ru/post/267295/


All Articles