From ad4d406a2dc21c7b9bc5af37d75e32eeae63a8eb Mon Sep 17 00:00:00 2001 From: kuwoyuki Date: Sat, 12 Apr 2025 13:39:20 +0600 Subject: [PATCH] first commit --- .gitignore | 1 + README.md | 135 +++++++++++ rigol_setup_crypter.py | 491 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 627 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 rigol_setup_crypter.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7154659 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +setup.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..69a1f2f --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# Rigol setup.stp Crypter + +A utility for encrypting, decrypting, and parsing Rigol DG[89]00 Pro Arbitrary Waveform Generator `setup.stp`. + +## Command-Line Options + +```sh +usage: rigol_setup_crypter.py [-h] (-e | -d | -p) (-t INPUT_TEXT | -f INPUT_FILE) [-k CPU_SERIAL] [-o FILEPATH] + +Encrypt or Decrypt Rigol DG[89]00 Pro setup.stp using the CPU SN. + +options: + -h, --help show this help message and exit + -e, --encrypt Encrypt plain text data + -d, --decrypt Decrypt hex string data + -p, --parse Parse Rigol CSV data string + -t, --text INPUT_TEXT + Input data as text (Plain for Encrypt/Parse, Hex for Decrypt) + -f, --file INPUT_FILE + Path to read input data from file + -k, --key CPU_SERIAL CPU Serial Number (Required for -e/-d modes) + -o, --output FILEPATH + Optional path to save output. Console if omitted +``` + +## Setup and Workflow + +### Prerequisites + +1. Install ADB (Android Debug Bridge) on your system: + + - **Linux**: `sudo apt install adb` (or equivalent for your distribution) + - **Windows**: Download and install [Android Platform Tools](https://developer.android.com/studio/releases/platform-tools) + +2. Connect your Rigol AFG to your local network and note its IP address + +### Extracting the Setup File + +1. Connect to your device via ADB: + + ``` + adb connect IP:55555 + ``` + + Replace `IP` with your device's IP address + +2. Pull the setup file from the device: + + ``` + adb pull /rigol/data/setup.stp + ``` + +3. Read your CPU serial: + + ``` + adb shell -- /rigol/shell/get_cpu_serial_num.sh + ``` + +4. **IMPORTANT**: Create a backup of the original file: + ``` + cp setup.stp setup.stp.backup + ``` + +### Modifying the Setup File + +1. Decrypt the setup file: + + ``` + python rigol_setup_crypter.py -d -f setup.stp -k YOUR_CPU_SERIAL -o setup.csv + ``` + + Replace `YOUR_CPU_SERIAL` with your device's CPU serial number + +2. Parse the file to better understand its structure: + + ``` + python rigol_setup_crypter.py -p -f setup.csv + ``` + +3. Edit the CSV file using a text editor of your choice + +4. Verify your changes by parsing again: + + ``` + python rigol_setup_crypter.py -p -f setup.csv + ``` + +5. Encrypt the modified file: + ``` + python rigol_setup_crypter.py -e -f setup.csv -k YOUR_CPU_SERIAL -o setup.stp.new + ``` + +### Uploading the Modified File + +1. Push the modified file back to the device: + + ``` + adb push setup.stp.new /rigol/data/setup.stp + ``` + +2. Restart your device to apply the changes + + ``` + adb shell -- reboot + ``` + +## Examples + +### Decrypt a setup file + +``` +python rigol_setup_crypter.py -d -f setup.stp -k d95ebe35672e20c2fc08 -o decrypted.csv +``` + +### Decrypt a text string and output to console + +``` +python rigol_setup_crypter.py -d -t "FBC6BDDB0E..." -k d95ebe35672e20c2fc08 +``` + +### Parse a decrypted CSV + +``` +python rigol_setup_crypter.py -p -f decrypted.csv +``` + +### Encrypt a modified CSV + +``` +python rigol_setup_crypter.py -e -f modified.csv -k d95ebe35672e20c2fc08 -o new_setup.stp +``` + +## Warning + +Keep a backup of the original `setup.csv`. diff --git a/rigol_setup_crypter.py b/rigol_setup_crypter.py new file mode 100644 index 0000000..20601c0 --- /dev/null +++ b/rigol_setup_crypter.py @@ -0,0 +1,491 @@ +#!/usr/bin/env python3 +# author: kuwoyuki +# Rigol DG[89]00 Pro AWG setup.stp crypter +import argparse +import binascii +import csv +import json +import io +import sys + +# Standard DES Tables (FIPS 46-3, 1-based indexing) +# fmt: off +IP_1based = [58,50,42,34,26,18,10,2,60,52,44,36,28,20,12,4,62,54,46,38,30,22,14,6,64,56,48,40,32,24,16,8,57,49,41,33,25,17,9,1,59,51,43,35,27,19,11,3,61,53,45,37,29,21,13,5,63,55,47,39,31,23,15,7] +FP_1based = [40,8,48,16,56,24,64,32,39,7,47,15,55,23,63,31,38,6,46,14,54,22,62,30,37,5,45,13,53,21,61,29,36,4,44,12,52,20,60,28,35,3,43,11,51,19,59,27,34,2,42,10,50,18,58,26,33,1,41,9,49,17,57,25] +E_1based = [32,1,2,3,4,5,4,5,6,7,8,9,8,9,10,11,12,13,12,13,14,15,16,17,16,17,18,19,20,21,20,21,22,23,24,25,24,25,26,27,28,29,28,29,30,31,32,1] +P_1based = [16,7,20,21,29,12,28,17,1,15,23,26,5,18,31,10,2,8,24,14,32,27,3,9,19,13,30,6,22,11,4,25] +PC1_1based = [57,49,41,33,25,17,9,1,58,50,42,34,26,18,10,2,59,51,43,35,27,19,11,3,60,52,44,36,63,55,47,39,31,23,15,7,62,54,46,38,30,22,14,6,61,53,45,37,29,21,13,5,28,20,12,4] +PC2_1based = [14,17,11,24,1,5,3,28,15,6,21,10,23,19,12,4,26,8,16,7,27,20,13,2,41,52,31,37,47,55,30,40,51,45,33,48,44,49,39,56,34,53,46,42,50,36,29,32] +S = [[14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7,0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8,4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0,15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13],[15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10,3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5,0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15,13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9],[10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8,13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1,13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7,1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12],[7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15,13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9,10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4,3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14],[2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9,14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6,4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14,11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3],[12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11,10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8,9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6,4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13],[4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1,13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6,1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2,6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12],[13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7,1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2,7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8,2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11]] +LEFT_SHIFTS = [1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1] +# fmt: on + + +def _bytes_to_bits(byte_data): + return [(byte >> i) & 1 for byte in byte_data for i in range(7, -1, -1)] + + +def _bits_to_bytes(bits): + byte_list = bytearray() + padded_len = (len(bits) + 7) // 8 * 8 + bits = bits + [0] * (padded_len - len(bits)) + for i in range(0, padded_len, 8): + byte_val = sum(bits[i + j] << (7 - j) for j in range(8)) + byte_list.append(byte_val) + return bytes(byte_list) + + +def _permute(block, table): + return [block[i - 1] for i in table] + + +def _xor(bits_a, bits_b): + return [a ^ b for a, b in zip(bits_a, bits_b)] + + +def _rotate_left(bits, n): + n = n % len(bits) + return bits[n:] + bits[:n] + + +# key & IV derivation +def _generate_iv_bits(): + """Generate the 64-bit IV from 'Rigoler'""" + iv_str = "Rigoler" # 7 chars + iv_bits_49 = [] + for char in iv_str: + char_val = ord(char) + char_7_bits_lsb = [(char_val >> i) & 1 for i in range(7)] + iv_bits_49.extend(char_7_bits_lsb[::-1]) # reverse for MSB-of-7 first + return iv_bits_49 + [0] * 15 # pad with 15 zeros to 64 bits + + +def _generate_round_keys(cpu_serial): + # key material: 48 zeros | 8 bits of char 0 | 8 zeros + effective_64_bits = [0] * 64 + effective_64_bits[48:56] = [(ord(cpu_serial[0]) >> i) & 1 for i in range(7, -1, -1)] + + key_56 = _permute(effective_64_bits, PC1_1based) + c, d = key_56[:28], key_56[28:] + round_keys = [] + for i in range(16): + c, d = _rotate_left(c, LEFT_SHIFTS[i]), _rotate_left(d, LEFT_SHIFTS[i]) + round_keys.append(_permute(c + d, PC2_1based)) + return round_keys + + +# DES stuff +def _des_f_function(r_32, k_48): + """DES Feistel function F(R, K).""" + xored = _xor(_permute(r_32, E_1based), k_48) + s_out_32 = [] + for i in range(8): + sub = xored[i * 6 : (i + 1) * 6] + row = (sub[0] << 1) | sub[5] + col = (sub[1] << 3) | (sub[2] << 2) | (sub[3] << 1) | sub[4] + val = S[i][row * 16 + col] + s_out_32.extend([(val >> k) & 1 for k in range(3, -1, -1)]) + return _permute(s_out_32, P_1based) + + +def _des_crypt_block(block_64bits, round_keys, is_encrypt): + """DES encrypt/decrypt single 64-bit block""" + l, r = ( + _permute(block_64bits, IP_1based)[:32], + _permute(block_64bits, IP_1based)[32:], + ) + keys = round_keys if is_encrypt else round_keys[::-1] + for k in keys: + l, r = r, _xor(l, _des_f_function(r, k)) + return _permute(r + l, FP_1based) # final swap included + + +# CBC mode and padding +def _pad(data_bytes): + """Pad data with space chars (0x20) to multiple of 8 bytes""" + rem = len(data_bytes) % 8 + if rem == 0: + # len already a multiple of 8, NO padding + return data_bytes + else: + # add padding to reach the next multiple of 8 + pad_len = 8 - rem + return data_bytes + bytes([0x20] * pad_len) + + +def _unpad(padded_bytes): + """Remove trailing space padding marked by first ' ' ???""" + marker_index = padded_bytes.find(b" ") + return padded_bytes[:marker_index] if marker_index != -1 else padded_bytes + + +def _des_cbc_crypt(data_bytes, iv_64bits, round_keys, is_encrypt): + """DES-CBC encrypt/decrypt with padding""" + if is_encrypt: + data_bytes = _pad(data_bytes) + result_bytes = bytearray() + prev_block_bits = iv_64bits + + for i in range(0, len(data_bytes), 8): + current_block_bits = _bytes_to_bits(data_bytes[i : i + 8]) + if is_encrypt: + processed_bits = _des_crypt_block( + _xor(current_block_bits, prev_block_bits), round_keys, True + ) + result_bytes.extend(_bits_to_bytes(processed_bits)) + prev_block_bits = processed_bits + else: # decrypt + decrypted_bits = _des_crypt_block(current_block_bits, round_keys, False) + result_bytes.extend(_bits_to_bytes(_xor(decrypted_bits, prev_block_bits))) + prev_block_bits = current_block_bits + return _unpad(bytes(result_bytes)) if not is_encrypt else bytes(result_bytes) + + +def DataDecryption(strEncryptedDataHex, cpu_serial): + """Decrypts hex string using DES-CBC""" + try: + encrypted_bytes = binascii.unhexlify(strEncryptedDataHex) + except binascii.Error as e: + raise ValueError(f"Invalid hex string provided: {e}") + round_keys, iv_bits = _generate_round_keys(cpu_serial), _generate_iv_bits() + decrypted_bytes = _des_cbc_crypt(encrypted_bytes, iv_bits, round_keys, False) + try: + return decrypted_bytes.decode("utf-8") + except UnicodeDecodeError: + return decrypted_bytes # rawdog bytes + + +def DataEncryption(strPlainData, cpu_serial): + """Encrypts string using DES-CBC. Returns uppercase hex""" + plaintext_bytes = strPlainData.encode("utf-8") + round_keys, iv_bits = _generate_round_keys(cpu_serial), _generate_iv_bits() + encrypted_bytes = _des_cbc_crypt(plaintext_bytes, iv_bits, round_keys, True) + return binascii.hexlify(encrypted_bytes).decode("ascii").upper() + + +def write_output(data, output_file=None, is_binary=False): + # w to file + if output_file: + mode = "wb" if is_binary else "w" + encoding = None if is_binary else "utf-8" + with open(output_file, mode, encoding=encoding) as f: + f.write(data) + print(f"Output successfully written to: {output_file}") + return + + # w to stdout + if is_binary: + print(data.decode("utf-8")) + else: + print(data) + + +def parse_rigol_csv(csv_string): + FIELD_NAMES = [ + "Manufacturer", + "InstrModel", + "InstrSN", + "CalibrationDate", + "SineMaxFreq", + "SquareMaxFreq", + "RampMaxFreq", + "PulseMaxFreq", + "ArbMaxFreq", + "HarmonicMaxFreq", + "MinFreq", + "HarmonicMinFreq", + "ARMSerial", + "MaxChannels", + "ArbWaveLenLicense", + "ArbWaveLenValidTime", + "DuoChanChannelValidTime", + ] + FREQ_FIELDS = [ + "SineMaxFreq", + "SquareMaxFreq", + "RampMaxFreq", + "PulseMaxFreq", + "ArbMaxFreq", + "HarmonicMaxFreq", + "MinFreq", + "HarmonicMinFreq", + ] + UHZ_TO_MHZ_FACTOR = 1e-12 # 1 / 1_000_000_000_000 + ARB_LICENSE_MAP = { + "DEF": "Default", + "MEM": "Memory Depth License", + "CHD": "Dual Channel License", + } + TIME_STATUS_MAP = {"Forever": "Active (No Expiration)", "Inactive": "Not Active"} + TIME_FIELDS = ["ArbWaveLenValidTime", "DuoChanChannelValidTime"] + + if not csv_string.strip(): + return None + + print("\nCSV header:") + print(",".join(FIELD_NAMES)) + print(csv_string) + + # parse CSV row + reader = csv.reader(io.StringIO(csv_string)) + row = next(reader, None) + if not row: + return None + + data = dict(zip(FIELD_NAMES, row)) + + # convert frequency fields + for field in FREQ_FIELDS: + if field in data and data[field].isdigit(): + uhz_value = int(data[field]) + data[f"{field}_MHz"] = uhz_value * UHZ_TO_MHZ_FACTOR # pyright: ignore [reportArgumentType] + + # map license code + if "ArbWaveLenLicense" in data and data["ArbWaveLenLicense"] in ARB_LICENSE_MAP: + data["ArbWaveLenLicense_Description"] = ARB_LICENSE_MAP[ + data["ArbWaveLenLicense"] + ] + + # map time fields + for field in TIME_FIELDS: + if field in data and data[field] in TIME_STATUS_MAP: + data[f"{field}_Status"] = TIME_STATUS_MAP[data[field]] + + return data + + +def run_parse_mode(args, input_data): + parsed_result = parse_rigol_csv(input_data.strip()) + if not parsed_result: + print("Error: Failed to parse CSV data.") + sys.exit(1) + + if args.output_file: + json_output = json.dumps(parsed_result, indent=2) + with open(args.output_file, "w") as f: + f.write(json_output) + print(f"Output written to: {args.output_file}") + return + + sections = [ + ( + "Instrument Information", + [ + ("Manufacturer", "Manufacturer"), + ("Model", "InstrModel"), + ("Serial Number", "InstrSN"), + ("Calibration Date", "CalibrationDate"), + ("ARM CPU Serial", "ARMSerial"), + ("Max Channels", "MaxChannels"), + ], + ), + ( + "Frequency Specifications", + [ + ("Sine Max Frequency", "SineMaxFreq"), + ("Square Max Frequency", "SquareMaxFreq"), + ("Ramp Max Frequency", "RampMaxFreq"), + ("Pulse Max Frequency", "PulseMaxFreq"), + ("Arb Max Frequency", "ArbMaxFreq"), + ("Harmonic Max Frequency", "HarmonicMaxFreq"), + ("Min Frequency", "MinFreq"), + ("Harmonic Min Frequency", "HarmonicMinFreq"), + ], + ), + ( + "Licensing Information", + [ + ("Arb Wavelength License", "ArbWaveLenLicense"), + ("Arb Wavelength Valid Time", "ArbWaveLenValidTime"), + ("Duo Channel Valid Time", "DuoChanChannelValidTime"), + ], + ), + ] + + for section_title, fields in sections: + print(f"\n{section_title}:") + section_has_data = False + + for label, key in fields: + if key not in parsed_result: + continue + + section_has_data = True + value = parsed_result[key] + output_line = f" {label}: {value}" + + # MHz conv if available + mhz_key = f"{key}_MHz" + if mhz_key in parsed_result: + mhz_value = parsed_result[mhz_key] + if isinstance(mhz_value, (int, float)): + mhz_str = ( + f"{mhz_value:.6f}" + if abs(mhz_value) > 1e-3 or mhz_value == 0 + else f"{mhz_value:.6e}" + ) + output_line += f" ({mhz_str} MHz)" + + desc_key = f"{key}_Description" + status_key = f"{key}_Status" + if desc_key in parsed_result: + output_line += f" ({parsed_result[desc_key]})" + elif status_key in parsed_result: + output_line += f" ({parsed_result[status_key]})" + + print(output_line) + + if not section_has_data: + print(" (No data)") + + +def write_output(data, output_file=None, is_binary=False): + # w to file + if output_file: + mode = "wb" if is_binary else "w" + encoding = None if is_binary else "utf-8" + with open(output_file, mode, encoding=encoding) as f: + f.write(data) + print(f"Output successfully written to: {output_file}") + return + + # w to stdout + if is_binary: + print(data.decode("utf-8")) + else: + print(data) + + +def main(): + parser = argparse.ArgumentParser( + description="Encrypt or Decrypt Rigol DG[89]00 Pro setup.stp using the CPU SN.", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog="""Examples: + Encrypt text to console: + python %(prog)s -e -t "RIGOL TECHNOLOGIES,DG821,..." "d95ebe35672e20c2fc08" + + Decrypt text to console: + python %(prog)s -d -t "FBC6BDDB0E..." "d95ebe35672e20c2fc08" + + Parse CSV (decrypted setup.stp) to console: + python %(prog)s -p -t "RIGOL TECHNOLOGIES,DG821,..." + + Encrypt from file to output file: + python %(prog)s -e -f input.txt "d95ebe35672e20c2fc08" -o setup.stp + + Decrypt from file to output file: + python %(prog)s -d -f encrypted.hex "d95ebe35672e20c2fc08" -o setup.stp.hex +""", + ) + + mode_group = parser.add_mutually_exclusive_group(required=True) + mode_group.add_argument( + "-e", + "--encrypt", + action="store_const", + dest="mode", + const="e", + help="Encrypt plain text data", + ) + mode_group.add_argument( + "-d", + "--decrypt", + action="store_const", + dest="mode", + const="d", + help="Decrypt hex string data", + ) + mode_group.add_argument( + "-p", + "--parse", + action="store_const", + dest="mode", + const="p", + help="Parse Rigol CSV data string", + ) + + input_group = parser.add_mutually_exclusive_group(required=True) + input_group.add_argument( + "-t", + "--text", + dest="input_text", + help="Input data as text (Plain for Encrypt/Parse, Hex for Decrypt)", + ) + input_group.add_argument( + "-f", "--file", dest="input_file", help="Path to read input data from file" + ) + + parser.add_argument( + "-k", + "--key", + dest="cpu_serial", + help="CPU Serial Number (Required for -e/-d modes)", + ) + parser.add_argument( + "-o", + "--output", + metavar="FILEPATH", + dest="output_file", + help="Optional path to save output. Console if omitted", + ) + + args = parser.parse_args() + + if args.mode in ["e", "d"] and not args.cpu_serial: + parser.error( + "-k/--key (CPU serial) is required for encryption and decryption modes" + ) + + # read input data from arg or file + if args.input_file: + with open(args.input_file, "r", encoding="utf-8") as f: + input_data = f.read() + else: + input_data = args.input_text + + if args.mode == "e": + # encrypt + print("Mode: Encrypt") + + if args.input_file: + print(f"Reading input from file: {args.input_file}") + + print(f"Input Data: '{input_data[:50]}{'...' if len(input_data) > 50 else ''}'") + print(f"CPU Serial: {args.cpu_serial}") + print("-" * 20) + + encrypted_result_hex = DataEncryption(input_data, args.cpu_serial) + + print("Encryption Complete.") + if not args.output_file: + print("\nEncrypted Hex Output:") + # always a hex str + write_output(encrypted_result_hex, args.output_file, is_binary=False) + + elif args.mode == "d": + # decrypt + print("Mode: Decrypt") + + if args.input_file: + print(f"Reading input from file: {args.input_file}") + + print( + f"Input Hex Data: '{input_data[:50]}{'...' if len(input_data) > 50 else ''}'" + ) + print(f"CPU Serial: {args.cpu_serial}") + print("-" * 20) + + decrypted_result = DataDecryption(input_data, args.cpu_serial) + + print("Decryption Complete.") + if not args.output_file: + print("\nDecrypted Data Output:") + + is_binary_output = isinstance(decrypted_result, bytes) + write_output(decrypted_result, args.output_file, is_binary=is_binary_output) + elif args.mode == "p": + run_parse_mode(args, input_data) + + +if __name__ == "__main__": + main()