import argparse
from argparse import RawTextHelpFormatter
import base64
import binascii
import os.path
import re
import socket
import struct
import time
import zlib
import requests
import sys
import string
import array
from tqdm import tqdm
from hashlib import sha256
from tabulate import tabulate
import xxtea
from struct import pack
from ecdsa import BRAINPOOLP256r1, SigningKey, VerifyingKey

class rou_params:
    fram = {
        'md5': 'aadc292fe4063a7ac392e3c3dde51e84',
        'offsets': [3120, 3204, 3206, 3207],
    }
    pat = {
        'resp': re.compile(rb'admin:\n(.+)\nOK\n', re.MULTILINE | re.DOTALL),
        'serial': re.compile(rb'RIGOL TECHNOLOGIES\$(.+?)\$(.+?)\$(.+?)\n\$(.+?)\$(.+?)\$(.+?)\$(.+?)$')
    }
    options = [
        #Bandwidth upgrades
        'BW1T2', 'BW1T3', 'BW1T5',
        'BW2T3', 'BW2T5',
        'BW3T5',
        'BW6T10', 'BW6T20', 'BW6T30', 
        'BW07T1', 'BW07T2', 'BW07T3', 'BW07T5',
        'BW10T20', 'BW10T30',
        'BW15T25', 'BW15T35',
        'BW20T30',
        'BW25T35',

        #Options
        'MSO', '4CH', '2RL', '5RL', 'BND', 'COMP', 'EMBD', 'AUTO', 'FLEX', 'AUDIO', 'SENSOR', 'AERO',
        'ARINC', 'DG', 'JITTER', 'MASK', 'PWR', 'DVM', 'CTR', 'AWG',

        #Educational license
        #'EDK'
    ]
    priv_key_name = 'priv.pem'
    block_header_format = '<IiIiI'

class rou_colors:
    ENDC = '\033[0m'
    HEADER = '\033[95m'
    OKBLUE = '\033[94m'
    OKCYAN = '\033[96m'
    OKGREEN = '\033[92m'
    WARNING = '\033[93m'
    FAIL = '\033[91m'
    BOLD = '\033[1m'
    UNDERLINE = '\033[4m'

class rou_helper:
    def bsconvert(self, s):
        if len(s) % 4 != 0:
            raise ValueError('string length not multiple of 4')
        f = '{}L'.format(len(s)//4)
        dw = struct.unpack('>'+f,s)
        return struct.pack('<'+f,*dw)

    def len_no_ansi(self, string):
        return len(re.sub(r'[\u001B\u009B][\[\]()#;?]*((([a-zA-Z\d]*(;[-a-zA-Z\d\/#&.:=?%@~_]*)*)?\u0007)|((\d{1,4}(?:;\d{0,4})*)?[\dA-PR-TZcf-ntqry=><~]))', '', string))

    def print_term(self, text, color, nocolortext="", end="\n", ret=False, return_and_count=False):
        if USE_COLORS:
            string = f"{color}{text}{rou_colors.ENDC}{nocolortext}"
        else:
            string = f"{text}{nocolortext}"
        if return_and_count:
            return string, self.len_no_ansi(string)
        if ret:
            return string
        print(string, end=end, flush=True)
        return self.len_no_ansi(string)

    def print_goto_prev_at_pos(self, pos):
        print(f"\033[F\033[{pos+1}G", end="", flush=True)

    def neg(self, val):
        return -1 * val

    def get_data(self, buf, off, size):
        block = buf[off:off + size]
        return block, off + size

    def file_read(self, filename):
        with open(filename, mode="rb") as f:
            return bytes(f.read())
        return None

    def file_write(self, ftype="_", data=None, stdout_print=True, exact_filename=False):
        time_exp = int(time.time() * 10000)
        filename = f"rigol_{ftype}_{time_exp}.data"
        if exact_filename:
            filename = ftype
        f = open(filename, "wb")
        f.write(data)
        f.close()
        if stdout_print:
            self.print_term("Saved", rou_colors.OKGREEN, f" {ftype} to '", end="")
            self.print_term(filename, rou_colors.OKCYAN, f"'")
        return

    def get_dw(self, buf, off):
        dw = struct.unpack_from('<I', buf, off)[0]
        return dw, off + 4

    def get_dws(self, buf, off):
        dw = struct.unpack_from('<i', buf, off)[0]
        return dw, off + 4

    def model_to_license_str(self, model):
        m_bstr = ''
        m_num = ''
        if DEBUG:
            self.print_term("Input model", rou_colors.OKCYAN, f"={model} | ", end="")
        m = re.match(r'([A-Za-z]+)(\d+)([A-Za-z]*)$', model)
        if m is not None:
            m_bstr = m.group(1)
            m_num = m.group(2)
            model = m_bstr + m_num
            model = model[:len(m_bstr)+1] + ( '0' * (len(m_num)-1))
        if DEBUG:
            self.print_term("Output model", rou_colors.OKCYAN, f"={model}")
        return model

    def bytediff_with_offset(self, old=b'', new=b''):
        old_ba = bytearray(old)
        new_ba = bytearray(new)
        diffs = []
        for i in range(0, len(old_ba)):
            if old_ba[i] != new_ba[i]:
                diffs.append({'offset': i, 'old': old_ba[i], 'new': new_ba[i]})
        return diffs


    def get_config_name(self, id_):
        names = {
            '04': 'CH4',
            '03': 'CH3',
            '02': 'CH2',
            '01': 'CH1',
            '82': '---',
            '0B': 'Rigol Scope',
            '12': 'Math2',
            '11': 'Math1',
            '13': 'Math3',
            '14': 'Math4',
            '16': 'REF 1-10',
            '2F': 'Network Config',
        }
        if names.get(id_):
            return names[id_]
        return id_


class rou_crypto:
    def __init__(self, helpers):
        self.helpers = helpers

    def encrypt_xxtea(self, buf, key, pl_zero):
        buf += b'\x00' * (pl_zero-2)
        delta = len(buf) % 4
        if delta:
            buf += b'\x00' * (4 - delta)
        return xxtea.encrypt(buf, key, padding=False)

    def calc_crc32(self, buf):
        return zlib.crc32(buf) & 0xFFFFFFFF

    def sign_option(self, opt):
        bb = bytearray()
        bb.extend(opt['model'].encode())
        bb.extend(opt['serial'].encode())
        bb.extend(opt['option'].encode())
        bb.extend(opt['version'].encode())
        bb.append(0x00)
        bb.append(0x00)
        dig = sha256(bytes(bb)).digest()

        prev_key = True
        if not os.path.exists(params.priv_key_name):
            sk = SigningKey.generate(curve=BRAINPOOLP256r1, hashfunc=sha256)
            self.helpers.file_write(params.priv_key_name, sk.to_pem(), stdout_print=True, exact_filename=True)
            prev_key = False
        else:
            sk = SigningKey.from_pem(self.helpers.file_read(params.priv_key_name))

        sign = sk.sign_digest_deterministic(dig)
        vk = sk.verifying_key
        vkk = b'04' + vk.to_string().hex().upper().encode()
        vk = VerifyingKey.from_string(binascii.unhexlify(vkk), curve=BRAINPOOLP256r1)

        assert vk.verify_digest(sign, dig)
        return binascii.hexlify(sign).upper(), vkk, prev_key

class rou_rigol:
    def __init__(self, helpers, crypto, params):
        self.helpers = helpers
        self.crypto = crypto
        self.params = params

    def read_block(self, buf, off):
        start = off
        id_, id_neg, data_size, data_size_neg, crc32 = struct.unpack_from(params.block_header_format, buf, off)
        off += struct.calcsize(params.block_header_format)

        assert ((id_ + id_neg) == 0) and ((data_size + data_size_neg) == 0)

        block_data, off = self.helpers.get_data(buf, off, data_size)
        crc32_real = self.crypto.calc_crc32(block_data)

        assert crc32_real == crc32

        return {
            'offset': start,
            'id': id_,
            'data': block_data,
            'crc32': crc32_real
        }, off

    def exec_cmd(self, ip_addr, cmd, need_res=True):
        while True:
            res = requests.post('http://%s/cgi-bin/changepwd.cgi' % ip_addr, data={'pass0': '', 'pass1': '; %s # "' % cmd})
            if res.status_code == 500:
                continue

            m = params.pat['resp'].match(res.content)
            if m is None and need_res:
                continue

            if need_res:
                grp = m.group(1)
                return grp.decode()
            else:
                return None

    def get_mod_ser_ver_mac(self, ip_addr):
        res = requests.post('http://%s/cgi-bin/welcome.cgi' % ip_addr)
        m = params.pat['serial'].match(res.content)

        if m is None:
            return None, None, None, None
        #model, serial, version, mac
        return m.group(1).decode(), m.group(2).decode(), m.group(3).decode(), m.group(4).decode()

    def get_publickey(self, ip_addr):
        self.helpers.print_term("READING", rou_colors.OKGREEN, " ", end="")
        self.helpers.print_term("public key", rou_colors.OKCYAN, " ...", end="")
        data = rigol.exec_cmd(ip_addr, 'cat /rigol/data/Key.data | base64')
        self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
        return base64.b64decode(data)

    def reboot(self, ip_addr):
        self.helpers.print_term("PREPARING TO REBOOT...", rou_colors.OKGREEN, end="\n")
        try:
            rigol.exec_cmd(ip_addr, 'reboot', need_res=False)
        except:
            pass

        time.sleep(3)
        return self.wait_for_device_online(ip_addr)

    def wait_for_device_online(self, ip_addr):
        self.helpers.print_term("**** REBOOTING NOW ****", rou_colors.OKGREEN, end="\n")
        self.helpers.print_term("**** WAITING For Device to Be Back Online ****", rou_colors.OKGREEN, end="\n")
        while True:
            try:
                with socket.create_connection((ip_addr, 5555), timeout=1):
                    break
            except OSError:
                time.sleep(0.01)
            self.helpers.print_term(".", rou_colors.ENDC, end="")
        self.helpers.print_term("REBOOT COMPLETED !\n", rou_colors.OKGREEN, "**** DEVICE ONLINE ****", end="\n")
        self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
        return

    def apply_new_key(self, ip_addr, new_key, diff_cfram, keytype="publickey"):
        self.helpers.print_term("PATCHING", rou_colors.OKGREEN, " ", end="")
        self.helpers.print_term("FRAM", rou_colors.OKCYAN, " binary...", end="")
        self.exec_cmd(ip_addr, 'echo -n -e \'\\x03\' > /tmp/byte1', need_res=False)
        self.exec_cmd(ip_addr, 'echo -n -e \'\\x3d\' > /tmp/byte2', need_res=False)
        self.exec_cmd(ip_addr, 'echo -n -e \'\\x5b\' > /tmp/byte3', need_res=False)
        self.exec_cmd(ip_addr, 'echo -n -e \'\\xe5\' > /tmp/byte4', need_res=False)
        self.exec_cmd(ip_addr, 'cp /rigol/tools/fram /rigol/tools/fram01', need_res=False)
        self.exec_cmd(ip_addr, 'chmod +x /rigol/tools/fram01', need_res=False)
        self.exec_cmd(ip_addr, 'dd if=/tmp/byte1 of=/rigol/tools/fram01 obs=1 seek=%d conv=notrunc' % self.params.fram['offsets'][0], need_res=False)
        self.exec_cmd(ip_addr, 'dd if=/tmp/byte2 of=/rigol/tools/fram01 obs=1 seek=%d conv=notrunc' % self.params.fram['offsets'][1], need_res=False)
        self.exec_cmd(ip_addr, 'dd if=/tmp/byte3 of=/rigol/tools/fram01 obs=1 seek=%d conv=notrunc' % self.params.fram['offsets'][2], need_res=False)
        self.exec_cmd(ip_addr, 'dd if=/tmp/byte4 of=/rigol/tools/fram01 obs=1 seek=%d conv=notrunc' % self.params.fram['offsets'][3], need_res=False)
        self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")

        l = self.helpers.print_term("Applying", rou_colors.OKGREEN, " ", end="")
        l += self.helpers.print_term("new", rou_colors.BOLD, " ", end="")
        l += self.helpers.print_term("FRAM", rou_colors.OKCYAN, " ...", end="")
        with tqdm(total=len(diff_cfram), leave=False) as pb:
            for i in diff_cfram:
                rigol.exec_cmd(ip_addr, '/rigol/tools/fram01 -w %x %02x' % (i['offset'], i['new']), need_res=False)
                pb.update(1)
        self.helpers.print_goto_prev_at_pos(l)
        self.helpers.print_term(".", rou_colors.ENDC, "", end="")
        if keytype == "publickey":
            self.exec_cmd(ip_addr, 'cp -f /rigol/data/Key.data /rigol/data/Key.data.bak', need_res=False)
            self.helpers.print_term(".", rou_colors.ENDC, "", end="")
            self.exec_cmd(ip_addr, 'echo -n %s | base64 -d > /rigol/data/Key.data' % base64.b64encode(new_key).decode(), need_res=False)
        elif keytype == "sysvendor":
            self.exec_cmd(ip_addr, 'cp -f /rigol/data/sysvendor.bin /rigol/data/sysvendor.bin.bak', need_res=False)
            self.helpers.print_term(".", rou_colors.ENDC, "", end="")
            self.exec_cmd(ip_addr, 'echo -n %s | base64 -d > /rigol/data/sysvendor.bin' % base64.b64encode(new_key).decode(), need_res=False)
        self.helpers.print_term(".", rou_colors.ENDC, "", end="")
        self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
        return

class rou_activation:
    def __init__(self, helpers, rigol, crypto):
        self.helpers = helpers
        self.rigol = rigol
        self.crypto = crypto

    def activate_option(self, ip_addr, code, line):
        self.helpers.print_term("Activating", rou_colors.OKGREEN, " ", end="")
        self.helpers.print_term(code, rou_colors.OKCYAN, end="")
        if DEBUG:
            self.helpers.print_term(" {0} ".format(line.decode()), rou_colors.ENDC, end="")
        self.helpers.print_term("...", rou_colors.ENDC, end="")

        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(1)
        s.connect((ip_addr, 5555))
        s.sendall(b':SYSTem:OPTion:INSTall %s\n' % line)
        s.close()

        status = self.get_lic_options_status(ip_addr, option=code)
        if status is None:
            self.helpers.print_term("NOT ACTIVATED", rou_colors.FAIL, "!")
        elif status.get('status') != "Forever":
            self.helpers.print_term("not activated", rou_colors.FAIL, "!")
        else:
            self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
        return

    def options_uninstall(self, ip_addr):
        self.helpers.print_term("UNINSTALLING", rou_colors.OKGREEN, " **** ACTIVATED **** ", end="")
        self.helpers.print_term("options", rou_colors.OKCYAN, "...", end="")
        s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        s.settimeout(1)
        s.connect((ip_addr, 5555))
        s.sendall(b':SYSTem:OPTion:UNINSTall\n')
        s.close()
        self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")

    def get_lic_options_status(self, ip_addr, option=None):
        items = []
        if DEBUG:
            self.helpers.print_term("Retrieving", rou_colors.OKGREEN, " ", end="")
            self.helpers.print_term("data", rou_colors.OKCYAN, f" from {ip_addr}...", end="")
        while True:
            res = requests.post('http://%s/cgi-bin/options.cgi' % ip_addr)
            if res.status_code == 500:
                continue
            items = res.content.decode().split('#')
            break

        if DEBUG:
            self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
        table = [['OPTION CODE', 'STATUS', 'DESCRIPTION']]
        actives = []
        for item in items:
            row = item.split('$')
            if option is not None and option == row[0]:
                return {'code': row[0], 'status': row[1], 'description': row[2]}
            table.append(row)
            if row[1] == 'Forever':
                actives.append(row[0])
        if option is not None:
            return None
        return list(set(params.options) - set(actives)), table

class rou_fram:
    def __init__(self, helpers, rigol, crypto, params):
        self.helpers = helpers
        self.rigol = rigol
        self.crypto = crypto
        self.params = params

    def check_fram_tool(self, ip_addr):
        self.helpers.print_term("checking", rou_colors.OKGREEN, " ", end="")
        self.helpers.print_term("/rigol/tools/fram", rou_colors.OKCYAN, " ...", end="")
        res = rigol.exec_cmd(ip_addr, 'md5sum /rigol/tools/fram').split(' ')[0]
        assert res == params.fram['md5'], '\nDifferent /rigol/tools/fram hash. You have to recalc fram offsets (params.fram["offsets"]) !'
        self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
        return

    def get_fram(self, ip_addr, size=0x800):
        l = self.helpers.print_term("READING", rou_colors.OKGREEN, " ", end="")
        l += self.helpers.print_term("FRAM", rou_colors.OKCYAN, " data ...")
        fram = bytearray()
        i = 0

        # Max 8k of fram are important for our tasks (usually 2k)
        with tqdm(total=size, leave=False) as pb:
            while i < size:
                res = rigol.exec_cmd(ip_addr, '/rigol/tools/fram -r %0x' % i)
                if res is None:
                    continue

                bb = binascii.unhexlify(res.replace(',', ''))
                fram.extend(bb)
                i += 0x10
                pb.update(0x10)
        self.helpers.print_goto_prev_at_pos(l)
        self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
        return bytes(fram)

    def get_fram_blocks(self, fram):
        buf = fram[0x100:]

        off = 0
        full_size, off = self.helpers.get_dw(buf, off)
        full_size_neg, off = self.helpers.get_dws(buf, off)
        assert (full_size + full_size_neg) == 0, "size + neg_size must be 0"

        items = {}

        while off < full_size:
            block, off = self.rigol.read_block(buf, off)
            if DEBUG:
                self.helpers.print_term("  offset", rou_colors.OKCYAN, f"={block['offset']:04X},", end="")
                self.helpers.print_term(" id", rou_colors.OKCYAN, f"={block['id']:04d},", end="")
                self.helpers.print_term(" data_sz", rou_colors.OKCYAN, f"={len(block['data']):04d},", end="")
                self.helpers.print_term(" data", rou_colors.OKCYAN, f"={binascii.hexlify(block['data']).decode():s},", end="")
                self.helpers.print_term(" crc32", rou_colors.OKCYAN, f"={block['crc32']:08X}")
            items[block['id']] = block
        if DEBUG:
            self.helpers.print_term("last read offset", rou_colors.OKBLUE, f"=0x{off:04X}")
        return items

    def replace_fram_key(self, fram, key_data, using_key={}, keytype="publickey"):
        items = self.get_fram_blocks(fram)
        fram_key = None
        dec = None
        if keytype == "publickey":
            for item in items.keys():
                try:
                    # Not all devices have public key at 1D (29). Searching for a correct pubkey
                    dect = xxtea.decrypt(items[item]['data'], using_key, padding=False)
                    if dect.decode().startswith('brainpoolP256r1;'):
                        fram_key = items[item]
                        dec = dect
                except ValueError:
                    # Decrypt failed, try the next block
                    pass
        elif keytype == "sysvendor":
            # hardcoded for now
            if items.get(2112) is None:
                self.helpers.print_term("key for fram id 2112 does not exist. please report", rou_colors.FAIL)
                exit(-1)
            fram_key = items.get(2112)

        assert fram_key is not None, f"{keytype} not found in fram. Please report this"

        if DEBUG:
            self.helpers.print_term("using key at ", rou_colors.OKBLUE, end="")
            self.helpers.print_term("offset", rou_colors.OKCYAN, f"=0x{fram_key['offset']:04X}, ", end="")
            self.helpers.print_term("id", rou_colors.OKCYAN, f"={fram_key['id']:04d}")

        if keytype == "publickey":
            pl_zero = len(re.search(r"\x00+$", dec.decode())[0])
            assert pl_zero % 2 == 0 , "0 padding in public key is not a multiple of 2"
            new_key = self.crypto.encrypt_xxtea(b'brainpoolP256r1;%s' % key_data, using_key, pl_zero)
        elif keytype == "sysvendor":
            new_key = key_data

        data = self.update_fram_in_memory(bytearray(fram), 0x100, fram_key, new_key)
        return bytes(data), new_key

    def update_fram_in_memory(self, buf, off, block, block_data):
        block_len = len(block_data)
        assert len(block['data']) == block_len, "generated key length ("+str(len(block_data))+") does not match stored key length ("+str(len(block['data']))+")"
        crc32 = self.crypto.calc_crc32(block_data)
        struct.pack_into(self.params.block_header_format, buf, off + block['offset'], block['id'], self.helpers.neg(block['id']), block_len, self.helpers.neg(block_len), crc32)
        block_data_off = off + block['offset'] + struct.calcsize(params.block_header_format)
        buf[block_data_off:block_data_off + block_len] = block_data
        return buf

    def parse_fram(self, data):
        crc1 = struct.unpack('<I', data[:4])[0]
        l = struct.unpack('<I', data[4:8])[0]
        crc2 = self.crypto.calc_crc32(data[8:8 + l])
        assert crc1 == crc2, f"setup data crc error {crc1:x} {crc2:x}"
        if DEBUG:
            self.helpers.print_term("\nFRAM Settings", rou_colors.OKGREEN)

        l += 8
        i = 8
        odata = []
        while i < l:
            offset = i + 0x800
            idx = data[i]
            i += 1
            uncomp = data[i]
            i += 1
            x1 = data[i]
            i += 1
            x2 = data[i]
            i += 1
            dlen = struct.unpack('<H', data[i:i+2])[0]
            i += 2
            clen = struct.unpack('<H', data[i:i+2])[0]
            i += 2
            alen = struct.unpack('<H', data[i:i+2])[0]
            i += 2
            x3 = struct.unpack('<H', data[i:i+2])[0]
            i += 2
            bd = data[i:i+clen]
            bdx = bd
            zdec = True if uncomp == 0 else False
            if zdec:
                try:
                    zr = zlib.decompressobj()
                    bd = zr.decompress(bd[4:], dlen)
                    if len(bd) != dlen:
                        i += alen
                        self.helpers.print_term(f"read {len(bd):x} {zr.unused_data}", rou_colors.BOLD)
                        continue
                except zlib.error as err:
                    self.helpers.print_term(f"zlib error {err}")
                    return
            cfgname = self.helpers.get_config_name(f"{idx:02X}")
            if DEBUG:
                # offset
                self.helpers.print_term("  offset", rou_colors.OKCYAN, f"={offset:04X},", end="")
                self.helpers.print_term(" id", rou_colors.OKCYAN, f"={idx:02X} {x3:02X} {x2:02X}{x1:02X},", end="")
                self.helpers.print_term(" zlib_dec", rou_colors.OKCYAN, f"={zdec:b},", end="")
                self.helpers.print_term(" uncomp_sz", rou_colors.OKCYAN, f"={dlen:04d},", end="")
                self.helpers.print_term(" comp_sz", rou_colors.OKCYAN, f"={clen:04d},", end="")
                self.helpers.print_term(" total_sz", rou_colors.OKCYAN, f"={alen:04d},", end="")
                self.helpers.print_term(" name", rou_colors.OKCYAN, f"={cfgname:s},", end="")
                self.helpers.print_term(" data", rou_colors.OKCYAN, f"={binascii.hexlify(bd).decode('utf-8'):s}")
            i += alen

class rou_sysvendor:
    def __init__(self, helpers, rigol, crypto):
        self.helpers = helpers
        self.rigol = rigol
        self.crypto = crypto

    def get_current_data(self, ip_addr):
        self.helpers.print_term("READING", rou_colors.OKGREEN, " ", end="")
        self.helpers.print_term("sysvendor.bin", rou_colors.OKCYAN, " ...", end="")
        data = rigol.exec_cmd(ip_addr, 'cat /rigol/data/sysvendor.bin | base64')
        self.helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
        return base64.b64decode(data)


    def find_key(self, ip_addr, sv, is_slow=False):
        apid = self.rigol.exec_cmd(ip_addr, 'pidof appEntry')

        self.rigol.exec_cmd(ip_addr, 'rm -f /rigol/webcontrol/webpages/asp_findkey', need_res=False)
        self.rigol.exec_cmd(ip_addr, 'rm -f /tmp/asp_findkey', need_res=False)

        t0 = time.time_ns()
        cmd = 'cat /proc/'+str(apid)+'/maps | grep -Fv ".so" | grep " 0 " | awk "{print \\$1}" |'
        if not is_slow:
            cmd += ' head -1 | '
        cmd += '( IFS="-"; while read a b; do dd if=/proc/'+str(apid)+'/mem bs=4096 skip=$(( 0x$a / 4096 )) count='
        if is_slow:
            cmd += '$(( (0x$b - 0x$a)/4096 )) >>/tmp/asp_findkey'
        else:
            cmd += '32 of=/tmp/asp_findkey'
        cmd += '; done )'
        self.rigol.exec_cmd(ip_addr, cmd, need_res=False)
        dump_time = time.time_ns() - t0

        t0 = time.time_ns()
        self.rigol.exec_cmd(ip_addr, 'ln -s /tmp/asp_findkey /rigol/webcontrol/webpages/asp_findkey', need_res=False)
        res = requests.get(f'http://{ip_addr}/asp_findkey')
        download_time = time.time_ns() - t0
        posk = res.content
        dec_posk = posk.decode('latin1')

        if is_slow:
            #this might not be correct
            rp = dec_posk.find("\x00\x32\x80\x31\x0A\x00\x00\x18")
            posk = posk[rp:rp+512+16]
        else:
            #string with sn **should** be after the actual key
            rp = dec_posk.find("RIGOL_")
            posk = posk[:rp]

        self.rigol.exec_cmd(ip_addr, 'rm -f /rigol/webcontrol/webpages/asp_findkey', need_res=False)
        self.rigol.exec_cmd(ip_addr, 'rm -f /tmp/asp_findkey', need_res=False)

        t0 = time.time_ns()
        with tqdm(total=len(posk)-15, leave=False) as pbar:
            for i in range(len(posk)-15):
                pbar.update(1)
                if posk[i:i+16].count(0) == 16:
                    continue
                dect = xxtea.decrypt(sv, posk[i:i+16], padding=False)
                if re.match(r'.*E_CFG_.*', dect.decode('latin1')):
                    helpers.file_write('dec_sysvendor_{0}'.format(helpers.bsconvert(posk[i:i+16]).hex().upper()), dect, stdout_print=False)
                    pbar.reset(len(posk)-15)
                    pbar.update(len(posk)-15)
                    return dect, posk[i:i+16], dump_time, download_time, time.time_ns() - t0, i
        return None, None, 0, 0, 0, -1

    def print_data(self, svd, xxtea_key):
        file_crc = int(svd[:12].decode('latin1'))
        file_len = int(svd[12:24].decode('latin1'))
        self.helpers.print_term("sysvendor.bin data:", rou_colors.OKGREEN)
        self.helpers.print_term("  size", rou_colors.OKCYAN, "={0} ".format(file_len), end="")
        if len(svd[24:]) == file_len:
            self.helpers.print_term("ok!", rou_colors.OKBLUE, " | ", end="")
        else:
            self.helpers.print_term("failed!", rou_colors.FAIL, " actual len={:} | ".format(len(svd[24:])), end="")
        self.helpers.print_term("crc32", rou_colors.OKCYAN, "={0:X} [{1}] ".format(file_crc, file_crc), end="")
        if file_crc == self.crypto.calc_crc32(svd[24:]):
            self.helpers.print_term("ok!", rou_colors.OKBLUE, "")
        else:
            self.helpers.print_term("failed!", rou_colors.FAIL, " actual crc={:08X} | ".format(self.crypto.calc_crc32(svd[24:])))

        tv = [[
            self.helpers.print_term('key_size', rou_colors.OKCYAN, end="", ret=True),
            self.helpers.print_term('key_data', rou_colors.OKCYAN, end="", ret=True),
            self.helpers.print_term('value_size', rou_colors.OKCYAN, end="", ret=True),
            self.helpers.print_term('value_crc', rou_colors.OKCYAN, end="", ret=True),
            self.helpers.print_term('value_str_size', rou_colors.OKCYAN, end="", ret=True),
            self.helpers.print_term('value_decrypted_data', rou_colors.OKCYAN, end="", ret=True),
        ]]

        i = 24
        while i < len(svd):
            svkvd=[]
            #key_size
            key_size=int(svd[i:i+4].decode('latin1'))
            svkvd.append(key_size)
            i += 4

            #key_data
            svkvd.append(svd[i:i+key_size].decode('latin1'))
            i += key_size

            #value_size
            val_size=int(svd[i:i+12].decode('latin1'))
            svkvd.append(val_size)
            i += 12

            #value_data
            val_data=svd[i:i+val_size]
            # CRC32 val_data[8:]
            crc32 = "%08X" % int.from_bytes(val_data[:4], "little")
            calc_crc32 = binascii.hexlify((self.crypto.calc_crc32(val_data[8:]) % (1<<32)).to_bytes(4)).decode('latin1').upper()
            if crc32 == calc_crc32:
                svkvd.append(self.helpers.print_term(str(crc32), rou_colors.OKGREEN, ret=True))
            else:
                svkvd.append(self.helpers.print_term("{0} / {1}".format(crc32, calc_crc32), rou_colors.FAIL, ret=True))
            dsize = int.from_bytes(val_data[4:8], "little") # string size
            asize = int.from_bytes(val_data[8:12], "little") # total size
            svkvd.append(asize)
            vdata = val_data[12:]
            decdata = xxtea.decrypt(vdata, xxtea_key, padding=False)
            dec_str = decdata[:asize]
            dec_extra = decdata[asize:]
            svkvd.append(dec_str)
            i += val_size
            tv.append(svkvd)
        print(tabulate(tv, headers='firstrow', tablefmt='fancy_grid'))
        return

    def change_data(self, svd, dev_key, act_key, kv):
        i = 0x0C
        i += 12
        pfvd = b''

        while i < len(svd):
            #key_size
            key_size=int(svd[i:i+4].decode('latin1'))
            i += 4

            #key_data
            key_name = svd[i:i+key_size].decode('latin1')
            i += key_size

            #value_size
            val_size=int(svd[i:i+12].decode('latin1'))
            i += 12

            #value_data
            val_data=svd[i:i+val_size]
            # CRC32 val_data[8:]

            crc32 = "%08X" % int.from_bytes(val_data[:4], "little")
            calc_crc32 = binascii.hexlify((self.crypto.calc_crc32(val_data[8:]) % (1<<32)).to_bytes(4)).decode('latin1').upper()

            dsize = int.from_bytes(val_data[4:8], "little") # total size
            asize = int.from_bytes(val_data[8:12], "little") # string size

            vdata = val_data[12:]
            decdata = xxtea.decrypt(vdata, act_key, padding=False)

            if kv.get(key_name) is not None:
                asize = len(kv[key_name])
                decdata = "\x00" * dsize
                decdata = kv.get(key_name) + decdata[asize:]

            encdata = xxtea.encrypt(decdata, act_key, padding=False)
            encdata_full = asize.to_bytes(4, byteorder='little') + encdata
            enc_crc32 = (self.crypto.calc_crc32(encdata_full) % (1<<32)).to_bytes(4, byteorder='little')
            new_val_data = enc_crc32 + dsize.to_bytes(4, byteorder='little') + encdata_full
            fvd = b"%04d%s%012d%s" % (key_size, key_name.encode('latin1'), len(new_val_data), new_val_data)
            pfvd += fvd

            dec_str = decdata[:asize]
            dec_extra = decdata[asize:]
            i += val_size
        helpers.print_term("-= NEW sysvendor.bin =-", rou_colors.OKBLUE)
        fd = (b"%012d" % crypto.calc_crc32(pfvd)) + (b"%012d" % len(pfvd)) + pfvd
        rigol_sysvendor.print_data(fd, act_key)
        return fd

    def write(self, ip_addr, svd, rigol_fram, rigol_fram_size):
        rigol_fram.check_fram_tool(ip_addr)
        fram = rigol_fram.get_fram(ip_addr, rigol_fram_size)
        new_cfram, new_key = rigol_fram.replace_fram_key(fram, svd, keytype="sysvendor")
        diff_cfram = self.helpers.bytediff_with_offset(fram, new_cfram)
        # apply new key to the oscilloscope
        rigol.apply_new_key(ip_addr, new_cfram, diff_cfram, keytype="sysvendor")
        rigol.reboot(ip_addr)
        return

helpers = rou_helper()
crypto = rou_crypto(helpers)
params = rou_params()
rigol = rou_rigol(helpers, crypto, params)
rigol_activation = rou_activation(helpers, rigol, crypto)
rigol_fram = rou_fram(helpers, rigol, crypto, params)
rigol_sysvendor = rou_sysvendor(helpers, rigol, crypto)

def main():
    parser = argparse.ArgumentParser(
        description=f'{rou_colors.OKCYAN}RIGOL Oscilloscope Tool{rou_colors.ENDC} for MSO5/6/7/8XXX v2.13b by {rou_colors.OKCYAN}asp{rou_colors.ENDC}',
        epilog="*********************************************************************\n"
        "                   SCROLL UP FOR HELP INFORMATION\n"
        "*********************************************************************\n"
        "****************************** CREDITS ******************************\n"
        "ORIGINAL VERSION BY DrMefistO\n"
        "---------------------------------------------------------------------\n"
        "LOTS OF THANKS TO :\n\n"
        "tv84                - Paving the path from the start\n"
        "                      Responsible for rigol_kg2.py\n\n"
        "SMAS                - Laying out the path to activation in an\n"
        "                      Easy to understand way.\n\n"
        "Seppletronics       - For working out the edited version of the file\n\n"
        "BTO                 - Assisting with Assertion Error\n"
        "                    - Minor script modifications resulting in\n"
        "                    - rigol_kg2_3_000.py\n"
        "                    - Rigol_MSO_LicensingUtility_2.09b\n"
        "                    - Cosmetic changes to scripts\n\n"
        "traxpalicaru (TrAx) - Donated his MSO8000 for testing\n\n"
        "asp                 - HUGE Effort in script progression\n"
        "                    - Opened the door to MSO7000 and MSO8000\n"
        "                    - Continued to work on ongoing Assertion Error\n"
        "                    - Optimized the script for faster activation\n"
        "                    - Responsible for...\n"
        "                    - rigol_mso_util_2.02a.py\n"
        "                    - rigol_mso_util_2.03a.py\n"
        "                    - rigol_mso_util_2.04a.py\n"
        "                    - rigol_mso_util_2.05b.py\n"
        "                    - rigol_mso_util_2.06b.py\n"
        "                    - rigol_mso_util_2.07b.py\n"
        "                    - rigol_mso_util_2.08b.py\n"
        "                    - rigol_mso_util_2.09b.py\n"
        "                    - rigol_mso_util_2.10b.py\n"
        "                    - rigol_mso_util_2.11b.py\n"
        "                    - rigol_mso_util_2.12b.py\n"
        "                    - rigol_mso_util_2.13b.py\n\n"         
        "Kyr                 - Pointing out offest issue 0x001D\n\n"
        "ALL OTHER MEMBERS   - That supplied feedback when needed\n\n"
        "DAVE JONES(EEVBLOG) - For doing all that he does\n"
        "*********************************************************************\n"
        "                   SCROLL UP FOR HELP INFORMATION\n"
        "*********************************************************************\n",
        formatter_class=RawTextHelpFormatter)

    onoffl = parser.add_mutually_exclusive_group()
    onoffl.add_argument("--offline", help="Offline Mode", default='--online' not in sys.argv, action="store_true")
    onoffl.add_argument("--online", help="**DEFAULT WHEN NOT --offline**", default='--offline' not in sys.argv, action="store_true")

    # Online Only Action
    onlactn = parser.add_argument_group("Online actions", "Online actions")
    onlactn.add_argument("ip_addr", help="Rigol IP-address", nargs='?', default=None)
    onlact = onlactn.add_mutually_exclusive_group(required='--offline' not in sys.argv)
    onlact.add_argument("-i", "--info", help="Print options status, model and serial then exit", action="store_true")
    onlact.add_argument("-e", "--ssh", help="Start sshd (rigol/rigol or root/Rigol201)", action="store_true")
    onlact.add_argument("-r", "--regen", help="Regenerate private key", action="store_true")
    onlact.add_argument("-a", "--activate", help="Activate with private key", action="store_true")
    onlact.add_argument("-f", "--save-fram", help="Save first 2k of FRAM to 'rigol_fram_XXXXXXXXXXXX.data'. For DEBUG purposes only", action="store_true")
    onlact.add_argument("-p", "--save-pk", help="Save SSH Pub Key to 'rigol_pubkey_XXXXXXXXXXXX.data'. For DEBUG purposes only", action="store_true")
    onlact.add_argument("--save-sysvendor", help="Save sysvendor to 'rigol_sysvendor_XXXXXXXXXXXX.data'. For DEBUG purposes only", action="store_true")
    onlact.add_argument("--write-sysvendor", help="Write sysvendor file name. For DEBUG purposes only", action="store_true")
    onlact.add_argument("--sysvendor-key", help="Try to find sysvendor.bin key", action="store_true")
    onlact.add_argument("-u", "--uninstall", help="Uninstall all options.", action="store_true")
    onlact.add_argument("--reboot", help="Reboots the device and waits for it to be back online", action="store_true")
    onlact.add_argument("--reset-web-pwd", help="Reset web user/password to admin/rigol", action="store_true")

    # Online Only Parameters
    onlprm = parser.add_argument_group("Online run", "Online parameters")
    onlprm.add_argument("--write-sysvendor-file", help="Write file to sysvendor.bin", metavar="sysvendor.bin")
    onlprm.add_argument("--with-config", help="Will get 8k from FRAM. This will be slower", action="store_true")
    onlprm.add_argument("--no-reboot", help="Do not reboot scope after regenerating keys", action="store_true")
    onlprm.add_argument("--start-sshd-on-boot", help="Start sshd on boot", action="store_true")
    onlprms = onlprm.add_mutually_exclusive_group()
    onlprms.add_argument("--sysvendor-key-fast", help="Find device sysvendor.bin key (fast method) (DEFAULT)", action="store_true")
    onlprms.add_argument("--sysvendor-key-slow", help="Find device sysvendor.bin key (slow method)", action="store_true")

    # Offline Only Action
    oglactn = parser.add_argument_group("Offline actions", "Offline actions")
    oflact = oglactn.add_mutually_exclusive_group(required='--offline' in sys.argv)
    oflact.add_argument("--cfram-file", help="Dummy run based contents read from cfram file export", metavar="rigol_fram_XXXXXXXXXXXX.data")
    oflact.add_argument("--sysvendor-file", help="Dummy run based contents read from sysvendor file export", metavar="sysvendor.bin")

    # Offline Only Parameters
    oflprm = parser.add_argument_group("Offline run", "Offline parameters")
    oflprm.add_argument("-m", "--model", help="Model Name", default=None, metavar="MSO5074")
    oflprm.add_argument("-s", "--serial", help="Serial Number", default=None, metavar="MS5A123456789")
    oflprm.add_argument("-o", "--options", help="Options", default=None, metavar="AUTO,AERO,AUDIO")
    oflprm.add_argument("--mac-address", help="MAC Address", default=None, metavar="00:19:AF:FF:FF:FF")

    # Both Online & Offline Parameters
    allprm = parser.add_argument_group("Parameters", "Online&Offline Parameters")
    allprm.add_argument("-d", "--debug", help="Enable debugging", action="store_true")
    allprm.add_argument("--no-color", help="Color output", action="store_true")
    allprm.add_argument("--use-sysvendor-key", help="Use Key for sysvendor.bin decryption", metavar="AAAAAAAABBBBBBBBCCCCCCCCDDDDDDDD")
    allprmk = allprm.add_mutually_exclusive_group()
    allprmk.add_argument("-k", "--keyver", help="Key version", default=1, type=int, choices=[1,2])
    allprmk.add_argument("--use-activation-key", help="Use Key for activation", metavar="039200010841084118C32104318639C7")

    args = parser.parse_args()

    global DEBUG
    global USE_COLORS
    if args.debug:
        DEBUG=True
    else:
        DEBUG=False
    if args.no_color:
        USE_COLORS = False
    else:
        USE_COLORS = True

    if args.online and args.ip_addr is None:
        helpers.print_term("ip_addr required when in online mode", rou_colors.FAIL)
        exit(-1)

    #Defaults to key 1
    act_key = helpers.bsconvert(binascii.unhexlify("039200010841084118C32104318639C7"))
    if args.keyver == 2:
        act_key = helpers.bsconvert(binascii.unhexlify("478AA88799A858950177007887888798"))
    if args.use_activation_key:
        if not all(c in string.hexdigits for c in args.use_activation_key):
            helpers.print_term("Error: --use-activation-key must be hex", rou_colors.FAIL)
            exit(-1)
        if len(args.use_activation_key) != 32:
            helpers.print_term("Error: --use-activation-key must be 16 bytes long (32 chars)", rou_colors.FAIL)
            exit(-1)
        act_key = helpers.bsconvert(binascii.unhexlify(args.use_activation_key))
    using_key = act_key

    if args.offline and args.sysvendor_file:
        if not os.path.isfile(args.sysvendor_file):
            helpers.print_term("--sysvendor-file does not exist!", rou_colors.FAIL)
            exit()
        if args.use_sysvendor_key is None:
            helpers.print_term("--use-sysvendor-key is required when in offline mode and --sysvendor-file exists", rou_colors.FAIL)
            exit()
        if not all(c in string.hexdigits for c in args.use_sysvendor_key):
            helpers.print_term("Error: --use_sysvendor_key must be hex", rou_colors.FAIL)
            exit(-1)
        if len(args.use_sysvendor_key) != 32:
            helpers.print_term("Error: --use_sysvendor_key must be 16 bytes long (32 chars)", rou_colors.FAIL)
            exit(-1)
        dev_key = helpers.bsconvert(binascii.unhexlify(args.use_sysvendor_key))
        svd = xxtea.decrypt(helpers.file_read(args.sysvendor_file), dev_key, padding=False)
        rigol_sysvendor.print_data(svd, act_key)

        if args.model or args.serial or args.mac_address:
            # New sysvendor data
            nsvd = rigol_sysvendor.change_data(svd, dev_key, act_key, {"E_CFG_MODEL_RAW": args.model, "E_CFG_SN_RAW": args.serial, "E_CFG_MAC": args.mac_address})
            ensvd = xxtea.encrypt(nsvd, dev_key, padding=False)
            helpers.file_write("sysvendor.bin_mod", ensvd, True)
            helpers.print_term("sysvendor file has been modified!", rou_colors.OKGREEN)
        return

    if args.offline and args.cfram_file is not None and os.path.isfile(args.cfram_file):
        if (args.model is None) or (args.serial is None) or (args.options is None):
            helpers.print_term("Dummy [--model,--serial and --options] are mandatory when running with cfram file", rou_colors.FAIL)
            exit(-1)

        model = args.model
        ser = args.serial
        ver = 'DUMMY'
        mac = '0F-F1-C1-A1-BA-BE'
        unavails = args.options.split(',')
        k_model = helpers.model_to_license_str(model)
    else:
        model, ser, ver, mac = rigol.get_mod_ser_ver_mac(args.ip_addr)

    if args.model is not None and args.online:
        k_model = args.model
    else:
        k_model = helpers.model_to_license_str(model)

    print(tabulate([['MODEL', 'SERIAL NUMBER', 'FIRMWARE VERSION', 'MAC ADDRESS', 'LICENSED MODEL'],[model, ser, ver, mac, k_model]], headers='firstrow', tablefmt='fancy_grid'))

    if args.options is None and args.online:
        #Get options from scope
        unavails, table = rigol_activation.get_lic_options_status(args.ip_addr)
        print(tabulate(table, headers='firstrow', tablefmt='fancy_grid'))

    #Read fram size
    fsize = 0x800
    if args.with_config:
        fsize = 0x2000

    if args.online:
        if args.info:
            return

        if args.ssh:
            helpers.print_term("Activating", rou_colors.OKGREEN, " ", end="")
            helpers.print_term("sshd", rou_colors.OKCYAN, " ...", end="")
            ssh_already_running = rigol.exec_cmd(args.ip_addr, 'pidof sshd | wc -l')
            if int(ssh_already_running) > 0:
                helpers.print_term("sshd is already running", rou_colors.OKBLUE, "", end="")
            else:
                rigol.exec_cmd(args.ip_addr, '/usr/sbin/sshd', need_res=False)
                helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")

            if args.start_sshd_on_boot:
                helpers.print_term(" ...", rou_colors.ENDC, end="")
                ssh_already_added = rigol.exec_cmd(args.ip_addr, 'cat /rigol/shell/start.sh | grep "/usr/sbin/sshd" | grep -v "^#" | wc -l')
                if int(ssh_already_added) > 0:
                    helpers.print_term("sshd is already added to start on boot", rou_colors.OKBLUE, " ...", end="")
                else:
                    rigol.exec_cmd(args.ip_addr, 'echo "\n/usr/sbin/sshd &" >> /rigol/shell/start.sh', need_res=False)
            helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
            return

        if args.reset_web_pwd:
            helpers.print_term("Updating", rou_colors.OKGREEN, " ", end="")
            helpers.print_term("web interface", rou_colors.OKCYAN, " user/password to ", end="")
            helpers.print_term("admin", rou_colors.OKCYAN, "/", end="")
            helpers.print_term("rigol", rou_colors.OKCYAN, " ...", end="")
            rigol.exec_cmd(args.ip_addr, 'echo "admin:rigol" > /rigol/data/user.conf', need_res=False)
            helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
            return

        if args.sysvendor_key:
            sv = rigol_sysvendor.get_current_data(args.ip_addr)
            l = helpers.print_term("Looking for", rou_colors.OKGREEN, " ", end="")
            l += helpers.print_term("sysvendor.bin", rou_colors.OKCYAN, " key... ", end="")
            l += helpers.print_term("--== Don't poke the bear. Please be patient ==--", rou_colors.BOLD)
            svd, svdkey, dump_time, download_time, dec_time, tchecks = rigol_sysvendor.find_key(args.ip_addr, sv, args.sysvendor_key_slow)
            helpers.print_goto_prev_at_pos(l)
            if svdkey is not None:
                helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
                helpers.print_term("Timers", rou_colors.OKGREEN, " - ", end="")
                helpers.print_term("memory dump", rou_colors.OKCYAN, ": %.04f sec | " % (dump_time / 1000000000), end="")
                helpers.print_term("download dump", rou_colors.OKCYAN, ": %.04f sec | " % (download_time / 1000000000), end="")
                helpers.print_term("find key", rou_colors.OKCYAN, ": %.04f sec" % (dec_time / 1000000000))
                helpers.print_term("Device XXTEA", rou_colors.OKGREEN, " key [", end="")
                helpers.print_term("%s" % (helpers.bsconvert(svdkey).hex().upper()), rou_colors.OKCYAN, "] was found after checking ", end="")
                helpers.print_term("%d" % tchecks, rou_colors.OKCYAN, " possible keys")
                helpers.print_term("Saving", rou_colors.OKGREEN, " decrypted ", end="")
                helpers.print_term("sysvendor.bin", rou_colors.OKCYAN, " locally...", end="")
                helpers.print_term("*** COMPLETED ***", rou_colors.OKGREEN, "!")
                rigol_sysvendor.print_data(svd, act_key)
            else:
                helpers.print_term("failed", rou_colors.FAIL, "!")
            return

        if args.write_sysvendor:
            if not args.write_sysvendor_file:
                helpers.print_term("--write-sysvendor-file param is missing!", rou_colors.FAIL)
                exit()
            if not os.path.isfile(args.write_sysvendor_file):
                helpers.print_term("--write-sysvendor-file does not exist!", rou_colors.FAIL)
                exit()
            rigol_sysvendor.write(args.ip_addr, helpers.file_read(args.write_sysvendor_file), rigol_fram, fsize)
            return

        if args.uninstall:
            rigol_activation.options_uninstall(args.ip_addr)
            rigol.reboot(args.ip_addr)
            return

        if args.save_fram:
            helpers.file_write("fram", rigol_fram.get_fram(args.ip_addr, fsize))
            return

        if args.save_pk:
            helpers.file_write("pubkey", rigol.get_publickey(args.ip_addr))
            return

        if args.save_sysvendor:
            helpers.file_write("sysvendor", rigol_sysvendor.get_current_data(args.ip_addr))
            return

        if args.reboot:
            rigol.reboot(args.ip_addr)
            return

        #Check fram tool
        rigol_fram.check_fram_tool(args.ip_addr)

    opts = []
    key_hex = None
    prev_key = True
    cfram = None

    # Delete previous priv.pem if --regen passed as an argument
    if args.regen and os.path.isfile('priv.pem'):
        os.remove('priv.pem')

    for option in unavails:
        opt_sign, key_hex, prev_key = crypto.sign_option({
            'model': model,
            'serial': ser,
            'option': option,
            'version': '1.0'
        })
        opts.append((option, opt_sign))

    opts = sorted(opts, key=lambda e: e[0])

    # --regen passed as an argument so we will regenerate a new private key and update cfram
    if args.regen:
        cfram = rigol_fram.get_fram(args.ip_addr, fsize)
    elif args.offline and args.cfram_file is not None:
        cfram = helpers.file_read(args.cfram_file)

    if cfram is not None:
        i = 7
        helpers.print_term("System Setup", rou_colors.OKGREEN)
        helpers.print_term("  Language: ", rou_colors.OKCYAN, f"{cfram[i+1]:X}" )
        helpers.print_term("  LoadLast: ", rou_colors.OKCYAN, f"{cfram[i+2]:X}" )
        helpers.print_term("  PowerStatus: ", rou_colors.OKCYAN, f"{cfram[i+3]:X}" )
#        helpers.print_term("  GPIB: ", rou_colors.OKCYAN, f"{cfram[i+4]:X}" )
        helpers.print_term("  Cycle count: ", rou_colors.OKCYAN, int.from_bytes(cfram[i+9:i+13], byteorder='big'))
#        helpers.print_term("  Cycle count: ", rou_colors.OKCYAN, int.from_bytes(cfram[i+5:i+13], byteorder='little'))
#        helpers.print_term("  Live count: ", rou_colors.OKCYAN, int.from_bytes(cfram[i+13:i+17], byteorder='little'))
        helpers.print_term("  Keep Imp: ", rou_colors.OKCYAN, f"{cfram[i+17]:X}" )
        helpers.print_term("  IPmode: ", rou_colors.OKCYAN, f"{cfram[i+18]:X}" )
        if len(cfram) >= 0x2000:
            rigol_fram.parse_fram(cfram[0x800:])

    if args.regen or (args.offline and cfram is not None):
        # replace key in memory
        helpers.print_term("ACTIVATION XXTEA", rou_colors.OKGREEN, " key [", end="")
        helpers.print_term("%s" % (helpers.bsconvert(using_key).hex().upper()), rou_colors.OKCYAN, "]")
        new_cfram, new_key = rigol_fram.replace_fram_key(cfram, key_hex, using_key, keytype="publickey")
        # generate diff between old and new cfram data
        diff_cfram = helpers.bytediff_with_offset(cfram, new_cfram)
        if args.online:
            # apply new key to the oscilloscope
            rigol.apply_new_key(args.ip_addr, new_cfram, diff_cfram, keytype="publickey")
            if args.no_reboot:
                return
            # reboot the scope
            rigol.reboot(args.ip_addr)

    if args.activate or args.regen:
        for opt in opts:
            code = opt[0].encode()
            rigol_activation.activate_option(args.ip_addr, code.decode(), b'%s-%s@%s' % (k_model.encode(), code, opt[1]))

    if args.online:
        _, table = rigol_activation.get_lic_options_status(args.ip_addr)
        print(tabulate(table, headers='firstrow', tablefmt='fancy_grid'))

if __name__ == '__main__':
    main()
