-
-
Save gtasteve/677bd5c1d9528a23b6c1bfd8f36e3e60 to your computer and use it in GitHub Desktop.
Load and decrypt Krux encrypted mnemonics from JSON, QR codes, save them as .txt and load as .txt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # The MIT License (MIT) | |
| # Copyright (c) 2021-2023 Krux contributors | |
| # Patched by Grok (April 2026) for better error messages + legacy format support only | |
| # NOTE: This patched version supports ONLY legacy Krux encrypted formats: | |
| # - Version 0 = PBKDF2_HMAC + AES-ECB | |
| # - Version 1 = PBKDF2_HMAC + AES-CBC | |
| # | |
| # Newer Krux firmware (2024+) uses the modern KEF (Krux Encryption Format) | |
| # with version bytes >= 2. This script does NOT support KEF. | |
| # If you get "Unsupported encryption version", decrypt directly on your Krux device. | |
| # Python requirements: pycryptodome, opencv-python, pyzbar, embit, qrcode | |
| # pip install pycryptodome opencv-python pyzbar embit qrcode | |
| # OS may require: zbar-tools (sudo apt install zbar-tools) | |
| import hashlib | |
| import base64 | |
| import argparse | |
| import json | |
| import cv2 | |
| from Crypto.Cipher import AES | |
| from pyzbar.pyzbar import decode | |
| from qrcode import QRCode | |
| from embit import bip39 | |
| PBKDF2_HMAC_ECB = 0 | |
| PBKDF2_HMAC_CBC = 1 | |
| AES_BLOCK_SIZE = 16 | |
| VERSION_MODE = { | |
| "AES-ECB": AES.MODE_ECB, | |
| "AES-CBC": AES.MODE_CBC, | |
| PBKDF2_HMAC_ECB: AES.MODE_ECB, | |
| PBKDF2_HMAC_CBC: AES.MODE_CBC, | |
| } | |
| VERSION_NUMBER = { | |
| "AES-ECB": PBKDF2_HMAC_ECB, | |
| "AES-CBC": PBKDF2_HMAC_CBC, | |
| } | |
| class AESCipher(object): | |
| """Helper for AES encrypt/decrypt""" | |
| def __init__(self, key, salt, iterations): | |
| self.key = hashlib.pbkdf2_hmac( | |
| "sha256", key.encode(), salt.encode(), iterations | |
| ) | |
| def decrypt(self, encrypted, mode, iv=None): | |
| """Decrypt a base64 using AES MODE_ECB and return the value decoded as string""" | |
| if iv: | |
| decryptor = AES.new(self.key, mode, iv) | |
| else: | |
| decryptor = AES.new(self.key, mode) | |
| load = decryptor.decrypt(encrypted).decode("utf-8") | |
| return load.replace("\x00", "") | |
| def decrypt_bytes(self, encrypted, mode, i_vector=None): | |
| """Decrypt and return value as bytes""" | |
| if i_vector: | |
| decryptor = AES.new(self.key, mode, i_vector) | |
| else: | |
| decryptor = AES.new(self.key, mode) | |
| return decryptor.decrypt(encrypted) | |
| class MnemonicStorage: | |
| """Handler of stored encrypted seeds""" | |
| def __init__(self, json_vault) -> None: | |
| self.stored = json_vault | |
| def list_mnemonics(self, sd_card=False): | |
| """List all seeds stored on a file""" | |
| mnemonic_ids = [] | |
| for mnemonic_id in self.stored: | |
| mnemonic_ids.append(mnemonic_id) | |
| return mnemonic_ids | |
| def decrypt(self, key, mnemonic_id): | |
| """Decrypt a selected encrypted mnemonic from a file""" | |
| try: | |
| encrypted_data = self.stored.get(mnemonic_id)["data"] | |
| iterations = self.stored.get(mnemonic_id)["key_iterations"] | |
| version = self.stored.get(mnemonic_id)["version"] | |
| except: | |
| return None | |
| # Legacy version check (same as QR path) | |
| if version not in VERSION_MODE: | |
| print(f"ERROR: Unsupported Krux encryption version in JSON: {version}") | |
| print(" This script only supports legacy formats (0 or 1).") | |
| print(" Your file was created with a newer Krux firmware.") | |
| return None | |
| data = base64.b64decode(encrypted_data) | |
| mode = VERSION_MODE[version] | |
| if mode == AES.MODE_ECB: | |
| encrypted_mnemonic = data | |
| iv = None | |
| else: | |
| encrypted_mnemonic = data[AES_BLOCK_SIZE:] | |
| iv = data[:AES_BLOCK_SIZE] | |
| decryptor = AESCipher(key, mnemonic_id, iterations) | |
| words = decryptor.decrypt(encrypted_mnemonic, mode, iv) | |
| return words | |
| class EncryptedQRCode: | |
| """Creates and decrypts encrypted mnemonic QR codes""" | |
| def __init__(self) -> None: | |
| self.mnemonic_id = None | |
| self.version = None | |
| self.iterations = None | |
| self.encrypted_data = None | |
| def public_data(self, data): | |
| """Parse and returns encrypted mnemonic QR codes public data""" | |
| mnemonic_info = "Encrypted QR Code:\n" | |
| try: | |
| id_length = int.from_bytes(data[:1], "big") | |
| self.mnemonic_id = data[1 : id_length + 1].decode("utf-8") | |
| mnemonic_info += "ID: " + self.mnemonic_id + "\n" | |
| self.version = int.from_bytes(data[id_length + 1 : id_length + 2], "big") | |
| # Improved version name lookup (prevents crash on unknown versions) | |
| version_name = next( | |
| (k for k, v in VERSION_NUMBER.items() if v == self.version), | |
| f"UNKNOWN ({self.version})" | |
| ) | |
| mnemonic_info += "Version: " + version_name + "\n" | |
| # DEBUG: show the raw version so user knows why it might fail | |
| print(f"DEBUG: Detected Krux encryption version byte = {self.version} ({version_name})") | |
| self.iterations = int.from_bytes(data[id_length + 2 : id_length + 5], "big") | |
| self.iterations *= 10000 | |
| mnemonic_info += "Key iter.: " + str(self.iterations) | |
| except Exception as e: | |
| print(f"ERROR: Failed to parse QR public data: {e}") | |
| return None | |
| extra_bytes = id_length + 5 # 1(id length byte) + 1(version) + 3(iterations) | |
| if self.version == 1: | |
| extra_bytes += 16 # Initial Vector size | |
| extra_bytes += 16 # Encrypted QR checksum is always 16 bytes | |
| len_mnemonic_bytes = len(data) - extra_bytes | |
| if len_mnemonic_bytes not in (16, 32): | |
| print(f"WARNING: Unexpected encrypted payload size ({len_mnemonic_bytes} bytes)") | |
| self.encrypted_data = data[id_length + 5 :] | |
| return mnemonic_info | |
| def decrypt(self, key): | |
| """Decrypts encrypted mnemonic QR codes - patched for legacy formats only""" | |
| # === PATCH: Safe version check for legacy formats only === | |
| if self.version not in VERSION_MODE: | |
| print(f"\nERROR: Unsupported Krux encryption version: {self.version}") | |
| print(" This script only supports legacy Krux formats (version 0 or 1).") | |
| print(" Your QR code was created with a newer Krux firmware that uses the modern KEF format.") | |
| print(" Recommendation:") | |
| print(" • Decrypt directly on your Krux device (scan + enter key)") | |
| print(" • Or export as a plain (unencrypted) SeedQR / text instead") | |
| return None | |
| mode = VERSION_MODE[self.version] | |
| if mode == AES.MODE_ECB: | |
| encrypted_mnemonic_data = self.encrypted_data | |
| i_vector = None | |
| else: | |
| encrypted_mnemonic_data = self.encrypted_data[AES_BLOCK_SIZE:] | |
| i_vector = self.encrypted_data[:AES_BLOCK_SIZE] | |
| success = False | |
| decryptor = AESCipher(key, self.mnemonic_id, self.iterations) | |
| decrypted_data = decryptor.decrypt_bytes( | |
| encrypted_mnemonic_data, mode, i_vector | |
| ) | |
| mnemonic_data = decrypted_data[:-AES_BLOCK_SIZE] | |
| checksum = decrypted_data[-AES_BLOCK_SIZE:] | |
| # Data validation | |
| if hashlib.sha256(mnemonic_data).digest()[:16] == checksum: | |
| success = True | |
| return mnemonic_data if success else None | |
| def scan(): | |
| """Opens a scan window and uses cv2 to detect and decode a QR code, returning its data""" | |
| vid = cv2.VideoCapture(0) | |
| qr_data = None | |
| while True: | |
| _, frame = vid.read() | |
| try: | |
| qr_data = decode(frame) | |
| except: | |
| qr_data = [] | |
| if qr_data: | |
| break | |
| cv2.imshow("frame", frame) | |
| if cv2.waitKey(1) & 0xFF == ord("q"): | |
| break | |
| vid.release() | |
| cv2.destroyAllWindows() | |
| return qr_data[0].data | |
| def qr_code(qr_data): | |
| from io import StringIO | |
| qr_code = QRCode() | |
| qr_code.add_data(qr_data) | |
| qr_string = StringIO() | |
| qr_code.print_ascii(out=qr_string, invert=True) | |
| print("Encrypted QR Code:") | |
| print(qr_string.getvalue()) | |
| parser = argparse.ArgumentParser( | |
| prog="krux_decrypt", | |
| description="Python script to decrypt Krux encrypted mnemonics (LEGACY formats only)", | |
| ) | |
| subparsers = parser.add_subparsers(help="sub-command help", dest="command") | |
| qr_parser = subparsers.add_parser("qr", help="scans and decrypts an encrypted QR code") | |
| file_parser = subparsers.add_parser( | |
| "json", help="loads and decrypts a mnemonic from a Krux encrypted .json file" | |
| ) | |
| file_parser.add_argument("--file", dest="json_file", help="path to .json file") | |
| file_saver_parser = subparsers.add_parser("save", help="Saves an encrypted mnemonic to a .txt file") | |
| file_saver_parser.add_argument("--file", dest="encrypted_mnemonic_file", help="path to .txt file") | |
| file_loader_parser = subparsers.add_parser("load-b64-decrypt", help="Loads and decrypts from a Base64 .txt file") | |
| file_loader_parser.add_argument("--file", dest="r_encrypted_mnemonic_file", help="path to .txt file") | |
| file_loader_qr_parser = subparsers.add_parser("load-b64-to-qr", help="Loads from Base64 .txt and shows QR") | |
| file_loader_qr_parser.add_argument("--file", dest="encrypted_to_qr", help="path to .txt file") | |
| args = parser.parse_args() | |
| if args.command == "qr": | |
| _ = input("Press enter to scan an encrypted mnemonic") | |
| scanned_data = scan() | |
| encrypted_qr = EncryptedQRCode() | |
| public_info = encrypted_qr.public_data(scanned_data.decode().encode("latin-1")) | |
| print(public_info) | |
| typed_key = input("\nEnter decryption key: ") | |
| binary_mnemonic = encrypted_qr.decrypt(typed_key) | |
| if binary_mnemonic: | |
| print("\nSUCCESS! Decrypted mnemonic:") | |
| print(bip39.mnemonic_from_bytes(binary_mnemonic)) | |
| else: | |
| print("\nFailed to decrypt (wrong key or unsupported format)") | |
| elif args.command == "json": | |
| if args.json_file is None: | |
| print("Please specify a file with --file") | |
| else: | |
| json_file = open(args.json_file, "r") | |
| json_vault = json.loads(json_file.read()) | |
| json_file.close() | |
| vault = MnemonicStorage(json_vault) | |
| print("Stored Mnemonics:") | |
| stored_mnemonics = vault.list_mnemonics() | |
| for i, mnemonic in enumerate(stored_mnemonics): | |
| print(str(i) + ":", mnemonic) | |
| valid_index = False | |
| while not valid_index: | |
| try: | |
| mnemonic_index = int( | |
| input("Enter the index of the mnemonic you wish to decrypt: ") | |
| ) | |
| valid_index = True | |
| except: | |
| print("Please enter an integer number") | |
| typed_key = input("Enter decryption key: ") | |
| binary_mnemonic = vault.decrypt(typed_key, stored_mnemonics[mnemonic_index]) | |
| if binary_mnemonic: | |
| print("\nSUCCESS! Decrypted mnemonic:") | |
| print(binary_mnemonic) | |
| else: | |
| print("Failed to decrypt") | |
| elif args.command == "save": | |
| if args.encrypted_mnemonic_file is None: | |
| print("Please specify a target file with --file") | |
| else: | |
| _ = input("Press enter to scan an encrypted mnemonic") | |
| scanned_data = scan() | |
| target_file = args.encrypted_mnemonic_file | |
| if not target_file.endswith(".txt"): | |
| target_file += ".txt" | |
| b64_data = base64.b64encode(scanned_data) | |
| print("Data length:", len(b64_data), "bytes") | |
| with open(target_file, "wb") as f: | |
| f.write(b64_data) | |
| print("Data saved to", target_file) | |
| elif args.command == "load-b64-decrypt": | |
| if args.r_encrypted_mnemonic_file is None: | |
| print("Please specify a file with --file") | |
| else: | |
| with open(args.r_encrypted_mnemonic_file, "rb") as f: | |
| b64_data = f.read() | |
| encrypted_data = base64.b64decode(b64_data) | |
| encrypted_qr = EncryptedQRCode() | |
| print(encrypted_qr.public_data(encrypted_data.decode().encode("latin-1"))) | |
| typed_key = input("Enter decryption key: ") | |
| binary_mnemonic = encrypted_qr.decrypt(typed_key) | |
| if binary_mnemonic: | |
| print("\nSUCCESS! Decrypted mnemonic:") | |
| print(bip39.mnemonic_from_bytes(binary_mnemonic)) | |
| else: | |
| print("Failed to decrypt") | |
| elif args.command == "load-b64-to-qr": | |
| if args.encrypted_to_qr is None: | |
| print("Please specify a file with --file") | |
| else: | |
| with open(args.encrypted_to_qr, "rb") as f: | |
| b64_data = f.read() | |
| encrypted_data = base64.b64decode(b64_data) | |
| encrypted_qr = EncryptedQRCode() | |
| encrypted_data = encrypted_data.decode().encode("latin-1") | |
| print(encrypted_qr.public_data(encrypted_data)) | |
| qr_code(encrypted_data) | |
| else: | |
| parser.print_help() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment