<jupyter_start><jupyter_text>TASK 3 - STREAMING CIPHER  1. Define an object that, starting from a random bit generator, generates random bytes.<jupyter_code>from lfsr import LFSR, int_to_bin, bin_to_int
from itertools import islice
import random

class PseudoRandomByteGenerator(object):
    ''' class docstring '''

    def __init__(self, seed=None, bit_generator=None, **kwargs):

        """
        self.poly = kwargs['poly']
        self.seed = seed
        """

        """
        if self.seed == None: 
            self.seed = '0' * max(self.poly)
        while len(self.seed) < max(self.poly): 
            self.seed = '0' + self.seed
        """
        
        self.bit_generator = bit_generator
        if self.bit_generator == None:
            self.bits = []
            

            self.lfsr_inst = LFSR(kwargs['poly'], seed)

        elif self.bit_generator == 'random':
            random.seed(seed)
            self.bits = 0

    def __iter__(self):
        return self

    def bit_to_bytes(self, s):
        return int(s, 2).to_bytes(len(s) // 8, byteorder='big')
    
    def __next__(self):
        if self.bit_generator == None: 
            self.bits = self.lfsr_inst.run_steps(N=8)   #get a list of 8 bits represented as true and false
            self.bits = ''.join(['1' if b==True else '0' for b in self.bits])   #convert the list of bits to a string of 0 and 1
            byte = int(self.bits, 2)
        elif self.bit_generator == 'random':
            self.bits = random.getrandbits(8)
            byte = self.bits
        return byte
prbg = PseudoRandomByteGenerator(poly=[3, 1]) # iterator
digits = [byte for byte in islice(prbg, 10)]
print(bytes(digits))
prbg = PseudoRandomByteGenerator(bit_generator='random') # iterator
digits = [byte for byte in islice(prbg, 10)]
print(bytes(digits)) # -> b'\x00x\xe7/\xdd7\xe1\\t\xc5'<jupyter_output>b'D\x03$S:\xda\xab\x95\x0e\x12'<jupyter_text>2. Define an object implementing a Stream Cipher that, given a Pseudo Random Byte Generator and a key, can encrypt and decrypt a message.<jupyter_code>class StreamCipher():
    ''' class docstring '''
    def __init__(self, key, prng=None, **kwargs):
        
        if prng is None:
            self.byte_gen = PseudoRandomByteGenerator(seed=key, **kwargs)
        else: 
            self.byte_gen = prng(key=key, **kwargs) 
    
    def encrypt(self, plaintext):
        N = len(plaintext)
        digits = [byte for byte in islice(self.byte_gen, N)]
        encrypted = [a ^ b for (a,b) in zip(bytes(digits), plaintext)]
        ciphertext = bytes(encrypted)
        return ciphertext
    
    def decrypt(self, ciphertext):
        N = len(ciphertext)
        digits = [byte for byte in islice(self.byte_gen, N)]
        decrypted = [a ^ b for (a,b) in zip(bytes(digits), ciphertext)]
        plaintext = bytes(encrypted)
        return plaintext
message = 'hello world!'
key = 0x0123456789ABCDEF
alice = StreamCipher(key, poly=[7, 1, 0])
plaintextA = message.encode('utf-8')
ciphertext = alice.encrypt(plaintextA)
bob = StreamCipher(key, poly=[7, 1, 0])
plaintextB = bob.encrypt(ciphertext)

print(plaintextA) # -> b'hello world!'
print(ciphertext) # -> b'\x9f\x03\xbcf\xfa\xdb`\xf6\x17\xce7\x88'
print(plaintextB) # -> b'hello world!'<jupyter_output>b'hello world!'
b'\xf9\xc7\xdf\xa8\xba\xc6\x80\x93\xf5M\x1c\xf7'
b'hello world!'<jupyter_text>3. Define an iterator that implements the A5/1 architecture and use it as bit generator in a Stream Cipher<jupyter_code>class A5_1():

    def __init__(self, key, frame, verbose=0):

        self.lfsr1 = LFSR(poly=[19, 18, 17, 14, 0], state=0)
        self.lfsr2 = LFSR(poly=[22, 21, 0], state=0)
        self.lfsr3 = LFSR(poly=[23, 22, 21, 8, 0], state=0)
        self.counter = 0
        self.verbose = verbose
        self.out = None

        if len(bin(key)[2:]) > 64: 
            raise Exception("The key must be 64 bit")
        else: 
            self.key = int_to_bin(key, 64)[::-1]
        
        if len(bin(frame)[2:])>22:
            raise Exception("The frame must be 22 bit")
        else: 
            self.frame = int_to_bin(frame, 22)[::-1]
        
        self.key_ins()
        self.frame_ins()
        self.key_mixing()

    def key_ins(self):
        for count in range(64):
            #feedback is None
            print('feedback:', self.lfsr1.feedback)
            self.lfsr1.feedback, self.lfsr2.feedback, self.lfsr3.feedback = self.key[count]^self.lfsr1.feedback, self.key[count]^self.lfsr2.feedback, self.key[count]^self.lfsr3.feedback
            if self.verbose == 1:
                if count == 0: 
                    print('{:>2} {:>3} {:>10} {:>12} {:>12} {:>10}'.format('iter', 'key', 'LFSR 1', 'LFSR2', 'LFSR3', 'out'))
                self.print_a5(count+1, mode='key')

    def frame_ins(self):
        for count in range(64):
            self.lfsr1.feedback, self.lfsr2.feedback, self.lfsr3.feedback = self.frame[count]^self.lfsr1.feedback, self.frame[count]^self.lfsr2.feedback, self.frame[count]^self.lfsr3.feedback
            self.__next__(key, frame=1)
            if self.verbose == 1: 
                if count == 0: 
                    print('{:>2} {:>3} {:>10} {:>12} {:>12} {:>10}'.format('iter', 'key', 'LFSR 1', 'LFSR2', 'LFSR3', 'out'))
                self.print_a5(count+1, mode='frame')
    
    def key_mixing(self):
        for count in range(100):
            self.__next__(key, frame=0)
            if self.verbose == 1: 
                if count == 0: 
                    print('{:>2} {:>3} {:>10} {:>12} {:>12} {:>10}'.format('iter', 'key', 'LFSR 1', 'LFSR2', 'LFSR3', 'out'))
                self.print_a5(count+1, mode='normal')
    
    def print_a5(self, count, mode): 
        state1 = self.lfsr1.state[2:]
        state2 = self.lfsr2.state[2:]
        state3 = self.lfsr3.state[2:]
        """
        state1 = bool_list_hex(self.lfsr1.state[2:])
        state2 = bool_list_hex(self.lfsr2.state[2:])
        state3 = bool_list_hex(self.lfsr3.state[2:])
        """
        if mode=='key':
            print('{:>2} {:>3} {:>12} {:>12} {:>12} {:>12}'.format(count, self.key[count-1], state1, state2, state3, self.out))
        elif mode == 'frame':
                print('{:>2} {:>3} {:>12} {:>12} {:>12} {:>12}'.format(count, self.frame[count-1], state1, state2, state3, self.out))
        elif mode == 'normal':
                print('{:>2} {:>3} {:>12} {:>12} {:>12} {:>12}'.format(count, state1, state2, state3, self.majority, self.out))

    def __iter__(self):
        return self
    
    def __next__(self, key_frame=0):

        if key_frame == 0:
            self.majority = not(floor(1/2+(int(self.lfsr1.state[8])+int(self.lfsr2.state[10])+int(self.lfsr3.state[10])-1/2)/3))
            if self.majority ^ self.lfsr1.state[8]:
                next(self.lfsr1)
            if self.majority ^ self.lfsr2.state[10]:
                next(self.lfsr2)
            if self.majority ^ self.lfsr3.state[8]:
                next(self.lfsr3)
        else: 
            next(self.lfsr1)
            next(self.lfsr2)
            next(self.lfsr3)
        self.out = self.lfsr1.output ^ self.lfsr2.output ^ self.lfsr3.output
        return int(self.out)

key, frame = 0x0123456789ABCDEF, 0x2F695A
a5 = A5_1(key, frame, verbose=1)
message = 'hello world!'
key = 0x0123456789ABCDEF

# create a StreamCipher instance for Alice and Bob
# no PRNG is specified, then LFSR is employed
alice = StreamCipher(key)
bob = StreamCipher(key)

plaintextA = message.encode('utf-8') # string to bytes
ciphertext = alice.encrypt(plaintextA) # encryption by Alice
plaintextB = bob.decrypt(ciphertext) # decryption by Bob

print(plaintextA) # -> b'hello world!'
print(ciphertext) # -> b'\x9f\x03\xbcf\xfa\xdb`\xf6\x17\xce7\x88'
print(plaintextB) # -> b'hello world!'<jupyter_output><empty_output><jupyter_text>4. Define an iterator that implements the RC4 architecture and use it in the Stream Cipher object to encrypt/decrypt a message<jupyter_code>class RC4(object): 
    def __init__(self, key, drop):
        self.key = lisy(key)
        self.P = []
        self.KSA()
        
        for i in range(drop):
            self.__next__()

    def KSA(self):
        j = 0 
        L = len(self.key)
        for i in range(265):
            self.P.append(i)
        
        for i in range(265):
            print('{:3d}{:3d}{:3d}{:3d}'.format(i, j, self.P[i], self.P[j]))
            j = (j + self.P[i] + self.key[i % L]) % 265
            self.P[i], self.P[j] = self.P[j], self.P[i]
        
    def __iter__(self):
        return self
    
    def __next__(self):
        i = 0
        j = 0
        i = (i+1) % 265
        j = (j+self.P[i]) % 265
        self.P[i], self.P[j] = self.P[j], self.P[i]
        self.K = self.P[(self.P[i]+self.P[j]) % 265]
        return self.K
message = 'hello world!’
key = b'<KEY>'

# create a StreamCipher instance for both Alice and Bob
alice = StreamCipher(key, prng=RC4, drop=10)
bob = StreamCipher(key, prng=RC4, drop=10)

plaintextA = message.encode('utf-8') # -> b'Hello world!'
ciphertext = alice.encrypt(plaintextA) # -> b'/\x9e\xf9\x83@\x81}\xa9\xd0\xd4\xd5\xf4'
plaintextB = bob.decrypt(ciphertext) # -> b'Hello world!'<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.15462239583333334, 'fraction_numerical': 0.04079861111111111, 'mean_word_length': 2.4314966492926287, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 64, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '4', 'dir': 'jupyter-structured-clean-dedup', 'id': '542649', 'license': 'MIT', 'n_tokens_mistral': 3605, 'n_tokens_neox': 3308, 'n_words': 726, 'path': '3.lfsr/lab3.ipynb', 'repo_name': 'regineru/90392-DataSecurity', 'size': '9227'}