Skip to content

Instantly share code, notes, and snippets.

@gtasteve
Forked from odudex/krux_decrypt.py
Last active April 16, 2026 16:39
Show Gist options
  • Select an option

  • Save gtasteve/677bd5c1d9528a23b6c1bfd8f36e3e60 to your computer and use it in GitHub Desktop.

Select an option

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
# 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