import numpy as np

# set print option for numpy to print all int as hex
np.set_printoptions(formatter={'int':hex})


def encrypt(plain_text, key, pdc='x', subByte = None, mix_const=None, RCon=None):
    if(subByte is None): 
        sub_byte = np.array([[0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76],
                            [0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0],
                            [0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15],
                            [0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75],
                            [0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84],
                            [0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF],
                            [0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8],
                            [0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2],
                            [0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73],
                            [0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB],
                            [0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79],
                            [0xE7, 0xCB, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08],
                            [0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A],
                            [0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E],
                            [0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF],
                            [0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16]])
    if(mix_const is None):
        mix_const = np.array([[0x02, 0x03, 0x01, 0x01],
                             [0x01, 0x02, 0x03, 0x01],
                             [0x01, 0x01, 0x02, 0x03],
                             [0x03, 0x01, 0x01, 0x02]])
    
    if(RCon is None):
        RCon = np.array([[0x01, 0x00, 0x00, 0x00],
                        [0x02, 0x00, 0x00, 0x00],
                        [0x04, 0x00, 0x00, 0x00],
                        [0x08, 0x00, 0x00, 0x00],
                        [0x10, 0x00, 0x00, 0x00],
                        [0x20, 0x00, 0x00, 0x00],
                        [0x40, 0x00, 0x00, 0x00],
                        [0x80, 0x00, 0x00, 0x00],
                        [0x1B, 0x00, 0x00, 0x00],
                        [0x36, 0x00, 0x00, 0x00]])
    round_key = key
    plain_text = plain_text.upper()

    # Initial state matrix
    state = np.empty([4, 4],  dtype=int)


    # Remove spaces and other characters to shorten Plain Text
    short_text = ''
    for c in plain_text:
        if ord(c) >= 65 and ord(c) <= 90:
            short_text += c

    # Add padding if necessery
    msg_length = len(short_text)
    for i in range(16 - msg_length):
        short_text += pdc.upper()

    # Convert the plain text to the initial state matrix
    rows = state.shape[0]
    cols = state.shape[1]

    for i in range(rows):
        for j in range(cols):
            state[j][i] = ord(short_text[i * rows + j])


    # 0. Add  pre-round key

    state = np.bitwise_xor(state, key)

    for round_no in range(10):
        # 1. SubByte

        for i in range(rows):
            for j in range(cols):
                x = state[i,j] % 0x10
                y = int(state[i,j] / 0x10)
                state[i][j] = sub_byte[y, x]


        # 2. Shift rows

        shifted = np.empty([rows, cols],  dtype=int)

        for i in range(rows):
            for j in range(cols):
                shifted[i, j] = state[i, (j + i) % cols]

        # 3. Mix columns
        if round_no < 9:
            mixed = np.matmul(mix_const, shifted) % 0xff


        # 4. Add round key

        # Rotate key
        temp = round_key.transpose()[cols-1]
        t = np.empty(cols, dtype=int)

        for i in range(cols):
            t[i] = temp[(i + 1) % cols]
        # SubByte key
        for i in range(cols):
            x = t[i] % 0x10
            y = int(t[i] / 0x10)
            t[i] = sub_byte[y, x]

        # X-OR with RCon
        t = np.bitwise_xor(t, RCon[round_no])

        # X-OR t and words from previous key
        current_key = np.empty([rows, cols], dtype=int)
        current_key[0] = np.bitwise_xor(t, round_key.transpose()[0])
        for i in range(1, cols):
            current_key[i] = np.bitwise_xor(current_key[i-1], round_key.transpose()[i])

        round_key = np.array(current_key.transpose())

        # Add roundKey to state
        final_state = np.bitwise_xor(mixed, round_key)

    return final_state.flatten('F')


def decrypt(cipher_text, key, subByte = None, inv_mix=None, RCon=None):
    
    if(subByte is None): 
        sub_byte = np.array([[0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76],
                            [0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0],
                            [0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15],
                            [0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75],
                            [0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84],
                            [0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF],
                            [0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8],
                            [0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2],
                            [0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73],
                            [0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB],
                            [0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79],
                            [0xE7, 0xCB, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08],
                            [0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A],
                            [0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E],
                            [0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF],
                            [0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16]])
    if(inv_mix is None):
        inv_mix = np.array([[0x0E, 0x0B, 0x0D, 0x09],
                             [0x09, 0x0E, 0x0B, 0x0D],
                             [0x0D, 0x09, 0x0E, 0x0B],
                             [0x0B, 0x0D, 0x09, 0x0E]])

    
    if(RCon is None):
        RCon = np.array([[0x01, 0x00, 0x00, 0x00],
                        [0x02, 0x00, 0x00, 0x00],
                        [0x04, 0x00, 0x00, 0x00],
                        [0x08, 0x00, 0x00, 0x00],
                        [0x10, 0x00, 0x00, 0x00],
                        [0x20, 0x00, 0x00, 0x00],
                        [0x40, 0x00, 0x00, 0x00],
                        [0x80, 0x00, 0x00, 0x00],
                        [0x1B, 0x00, 0x00, 0x00],
                        [0x36, 0x00, 0x00, 0x00]])
    
    # convert cipher text to state
    state = np.empty([4, 4],  dtype=int)
    
    rows = state.shape[0]
    cols = state.shape[1]

    for i in range(rows):
        for j in range(cols):
            state[j][i] = cipher_text[i*cols + j]
    
    # create Round Key
    round_key = np.empty([11, 4, 4], dtype=int)
    round_key[0] = key
    
    for round_n in range(1, 11):
        # Rotate key
        temp = round_key[round_n-1].transpose()[cols-1]
        t = np.empty(cols, dtype=int)

        for i in range(cols):
            t[i] = temp[(i + 1) % cols]
        # SubByte key
        for i in range(cols):
            x = t[i] % 0x10
            y = int(t[i] / 0x10)
            t[i] = sub_byte[y, x]

        # X-OR with RCon
        t = np.bitwise_xor(t, RCon[round_n-1])
        # X-OR t and words from previous key
        current_key = np.empty([rows, cols], dtype=int)
        current_key[0] = np.bitwise_xor(t, round_key[round_n-1].transpose()[0])
        for i in range(1, cols):
            current_key[i] = np.bitwise_xor(current_key[i-1], round_key[round_n-1].transpose()[i])

        round_key[round_n] = np.array(current_key.transpose())
        
    # 0. Add initial Round Key
    state = np.bitwise_xor(state, round_key[10])
    
    for round_n in range(10):
        
        # 1. Inverse shift rows
        
        state_1 = np.empty([rows, cols],  dtype=int)

        for i in range(rows):
            for j in range(cols):
                state_1[rows-i-1, j] = state[rows-i-1, (j + i + 1) % cols]
        
        # 2. Inverse SubByte
        
        for i in range(rows):
            for j in range(cols):
                pos = np.where(sub_byte == state_1[i][j])
                state_1[i][j] = pos[0][0] * 0x10 + pos[1][0]
        
        # 3. Add Round Key
        
        state_2 = np.bitwise_xor(state_1, round_key[9 - round_n])
        
        # 4. Inverse Mix Column
        
        if round_n < 9:
            state_3 = np.matmul(inv_mix, state_2) % 0xff
    
    final_state = state_3.flatten('F')
    
    return final_state


import numpy as np

# set print option for numpy to print all int as hex
np.set_printoptions(formatter={'int':hex})


def encrypt(plain_text, key, pdc='x', subByte = None, mix_const=None, RCon=None):
    if(subByte is None): 
        sub_byte = np.array([[0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76],
                            [0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0],
                            [0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15],
                            [0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75],
                            [0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84],
                            [0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF],
                            [0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8],
                            [0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2],
                            [0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73],
                            [0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB],
                            [0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79],
                            [0xE7, 0xCB, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08],
                            [0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A],
                            [0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E],
                            [0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF],
                            [0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16]])
    if(mix_const is None):
        mix_const = np.array([[0x02, 0x03, 0x01, 0x01],
                             [0x01, 0x02, 0x03, 0x01],
                             [0x01, 0x01, 0x02, 0x03],
                             [0x03, 0x01, 0x01, 0x02]])
    
    if(RCon is None):
        RCon = np.array([[0x01, 0x00, 0x00, 0x00],
                        [0x02, 0x00, 0x00, 0x00],
                        [0x04, 0x00, 0x00, 0x00],
                        [0x08, 0x00, 0x00, 0x00],
                        [0x10, 0x00, 0x00, 0x00],
                        [0x20, 0x00, 0x00, 0x00],
                        [0x40, 0x00, 0x00, 0x00],
                        [0x80, 0x00, 0x00, 0x00],
                        [0x1B, 0x00, 0x00, 0x00],
                        [0x36, 0x00, 0x00, 0x00]])
    round_key = key
    plain_text = plain_text.upper()

    # Initial state matrix
    state = np.empty([4, 4],  dtype=int)


    # Remove spaces and other characters to shorten Plain Text
    short_text = ''
    for c in plain_text:
        if ord(c) >= 65 and ord(c) <= 90:
            short_text += c

    # Add padding if necessery
    msg_length = len(short_text)
    for i in range(16 - msg_length):
        short_text += pdc.upper()

    # Convert the plain text to the initial state matrix
    rows = state.shape[0]
    cols = state.shape[1]

    for i in range(rows):
        for j in range(cols):
            state[j][i] = ord(short_text[i * rows + j])


    # 0. Add  pre-round key

    state = np.bitwise_xor(state, key)

    for round_no in range(10):
        # 1. SubByte

        for i in range(rows):
            for j in range(cols):
                x = state[i,j] % 0x10
                y = int(state[i,j] / 0x10)
                state[i][j] = sub_byte[y, x]


        # 2. Shift rows

        shifted = np.empty([rows, cols],  dtype=int)

        for i in range(rows):
            for j in range(cols):
                shifted[i, j] = state[i, (j + i) % cols]

        # 3. Mix columns
        if round_no < 9:
            mixed = np.matmul(mix_const, shifted) % 0xff


        # 4. Add round key

        # Rotate key
        temp = round_key.transpose()[cols-1]
        t = np.empty(cols, dtype=int)

        for i in range(cols):
            t[i] = temp[(i + 1) % cols]
        # SubByte key
        for i in range(cols):
            x = t[i] % 0x10
            y = int(t[i] / 0x10)
            t[i] = sub_byte[y, x]

        # X-OR with RCon
        t = np.bitwise_xor(t, RCon[round_no])

        # X-OR t and words from previous key
        current_key = np.empty([rows, cols], dtype=int)
        current_key[0] = np.bitwise_xor(t, round_key.transpose()[0])
        for i in range(1, cols):
            current_key[i] = np.bitwise_xor(current_key[i-1], round_key.transpose()[i])

        round_key = np.array(current_key.transpose())

        # Add roundKey to state
        final_state = np.bitwise_xor(mixed, round_key)

    return final_state.flatten('F')


def decrypt(cipher_text, key, subByte = None, inv_mix=None, RCon=None):
    
    if(subByte is None): 
        sub_byte = np.array([[0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76],
                            [0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0],
                            [0xB7, 0xFD, 0x93, 0x26, 0x36, 0x3F, 0xF7, 0xCC, 0x34, 0xA5, 0xE5, 0xF1, 0x71, 0xD8, 0x31, 0x15],
                            [0x04, 0xC7, 0x23, 0xC3, 0x18, 0x96, 0x05, 0x9A, 0x07, 0x12, 0x80, 0xE2, 0xEB, 0x27, 0xB2, 0x75],
                            [0x09, 0x83, 0x2C, 0x1A, 0x1B, 0x6E, 0x5A, 0xA0, 0x52, 0x3B, 0xD6, 0xB3, 0x29, 0xE3, 0x2F, 0x84],
                            [0x53, 0xD1, 0x00, 0xED, 0x20, 0xFC, 0xB1, 0x5B, 0x6A, 0xCB, 0xBE, 0x39, 0x4A, 0x4C, 0x58, 0xCF],
                            [0xD0, 0xEF, 0xAA, 0xFB, 0x43, 0x4D, 0x33, 0x85, 0x45, 0xF9, 0x02, 0x7F, 0x50, 0x3C, 0x9F, 0xA8],
                            [0x51, 0xA3, 0x40, 0x8F, 0x92, 0x9D, 0x38, 0xF5, 0xBC, 0xB6, 0xDA, 0x21, 0x10, 0xFF, 0xF3, 0xD2],
                            [0xCD, 0x0C, 0x13, 0xEC, 0x5F, 0x97, 0x44, 0x17, 0xC4, 0xA7, 0x7E, 0x3D, 0x64, 0x5D, 0x19, 0x73],
                            [0x60, 0x81, 0x4F, 0xDC, 0x22, 0x2A, 0x90, 0x88, 0x46, 0xEE, 0xB8, 0x14, 0xDE, 0x5E, 0x0B, 0xDB],
                            [0xE0, 0x32, 0x3A, 0x0A, 0x49, 0x06, 0x24, 0x5C, 0xC2, 0xD3, 0xAC, 0x62, 0x91, 0x95, 0xE4, 0x79],
                            [0xE7, 0xCB, 0x37, 0x6D, 0x8D, 0xD5, 0x4E, 0xA9, 0x6C, 0x56, 0xF4, 0xEA, 0x65, 0x7A, 0xAE, 0x08],
                            [0xBA, 0x78, 0x25, 0x2E, 0x1C, 0xA6, 0xB4, 0xC6, 0xE8, 0xDD, 0x74, 0x1F, 0x4B, 0xBD, 0x8B, 0x8A],
                            [0x70, 0x3E, 0xB5, 0x66, 0x48, 0x03, 0xF6, 0x0E, 0x61, 0x35, 0x57, 0xB9, 0x86, 0xC1, 0x1D, 0x9E],
                            [0xE1, 0xF8, 0x98, 0x11, 0x69, 0xD9, 0x8E, 0x94, 0x9B, 0x1E, 0x87, 0xE9, 0xCE, 0x55, 0x28, 0xDF],
                            [0x8C, 0xA1, 0x89, 0x0D, 0xBF, 0xE6, 0x42, 0x68, 0x41, 0x99, 0x2D, 0x0F, 0xB0, 0x54, 0xBB, 0x16]])
    if(inv_mix is None):
        inv_mix = np.array([[0x0E, 0x0B, 0x0D, 0x09],
                             [0x09, 0x0E, 0x0B, 0x0D],
                             [0x0D, 0x09, 0x0E, 0x0B],
                             [0x0B, 0x0D, 0x09, 0x0E]])

    
    if(RCon is None):
        RCon = np.array([[0x01, 0x00, 0x00, 0x00],
                        [0x02, 0x00, 0x00, 0x00],
                        [0x04, 0x00, 0x00, 0x00],
                        [0x08, 0x00, 0x00, 0x00],
                        [0x10, 0x00, 0x00, 0x00],
                        [0x20, 0x00, 0x00, 0x00],
                        [0x40, 0x00, 0x00, 0x00],
                        [0x80, 0x00, 0x00, 0x00],
                        [0x1B, 0x00, 0x00, 0x00],
                        [0x36, 0x00, 0x00, 0x00]])
    
    # convert cipher text to state
    state = np.empty([4, 4],  dtype=int)
    
    rows = state.shape[0]
    cols = state.shape[1]

    for i in range(rows):
        for j in range(cols):
            state[j][i] = cipher_text[i*cols + j]
    
    # create Round Key
    round_key = np.empty([11, 4, 4], dtype=int)
    round_key[0] = key
    
    for round_n in range(1, 11):
        # Rotate key
        temp = round_key[round_n-1].transpose()[cols-1]
        t = np.empty(cols, dtype=int)

        for i in range(cols):
            t[i] = temp[(i + 1) % cols]
        # SubByte key
        for i in range(cols):
            x = t[i] % 0x10
            y = int(t[i] / 0x10)
            t[i] = sub_byte[y, x]

        # X-OR with RCon
        t = np.bitwise_xor(t, RCon[round_n-1])
        # X-OR t and words from previous key
        current_key = np.empty([rows, cols], dtype=int)
        current_key[0] = np.bitwise_xor(t, round_key[round_n-1].transpose()[0])
        for i in range(1, cols):
            current_key[i] = np.bitwise_xor(current_key[i-1], round_key[round_n-1].transpose()[i])

        round_key[round_n] = np.array(current_key.transpose())
        
    # 0. Add initial Round Key
    state = np.bitwise_xor(state, round_key[10])
    
    for round_n in range(10):
        
        # 1. Inverse shift rows
        
        state_1 = np.empty([rows, cols],  dtype=int)

        for i in range(rows):
            for j in range(cols):
                state_1[rows-i-1, j] = state[rows-i-1, (j + i + 1) % cols]
        
        # 2. Inverse SubByte
        
        for i in range(rows):
            for j in range(cols):
                pos = np.where(sub_byte == state_1[i][j])
                state_1[i][j] = pos[0][0] * 0x10 + pos[1][0]
        
        # 3. Add Round Key
        
        state_2 = np.bitwise_xor(state_1, round_key[9 - round_n])
        
        # 4. Inverse Mix Column
        
        if round_n < 9:
            state_3 = np.matmul(inv_mix, state_2) % 0xff
    
    final_state = state_3.flatten('F')
    
    return final_state


# set print option for numpy to print all int as hex # Initial state matrix # Remove spaces and other characters to shorten Plain Text # Add padding if necessery # Convert the plain text to the initial state matrix # 0. Add  pre-round key # 1. SubByte # 2. Shift rows # 3. Mix columns # 4. Add round key # Rotate key # SubByte key # X-OR with RCon # X-OR t and words from previous key # Add roundKey to state # convert cipher text to state # create Round Key # Rotate key # SubByte key # X-OR with RCon # X-OR t and words from previous key # 0. Add initial Round Key # 1. Inverse shift rows # 2. Inverse SubByte # 3. Add Round Key # 4. Inverse Mix Column