mcdouglasx (OP)
|
 |
June 03, 2025, 06:18:30 PM Last edit: June 03, 2025, 07:00:27 PM by mcdouglasx |
|
I was scrolling through the forum and found a post about adding an extra layer of security to seed phrases. It sounded interesting, but the catch was that you’d need to store a separate key to decrypt it, which, let’s be real, is easy to lose or forget over time. So, I thought, why not skip the whole extra key thing and just use personal questions instead? That way, the owner doesn’t have to remember some random key, just answers they already know. To make it even harder to crack, I threw in a lucky number, something personal like a birthday, passport number, or another piece of info that’s tough for strangers to guess. I also built in a feature where the encrypted seed can double as a decoy, but right now, that only works with BIP39 wallets. Electrum is trickier because the encryption messes with the final word, making the decrypted version invalid. I kind of fixed that by leaving the last word unencrypted and tacking it onto the end of the passphrase. On top of that, I cranked up the encryption with 60,000,000 iterations to make brute-force attacks practically impossible. So far, it works fine for BIP39 wallets, but for Electrum, while the encryption itself is solid, using it as a decoy isn’t as straightforward. python requirement python script import os import re import hashlib import math import unicodedata from random import randrange from typing import Optional, Dict, Sequence, Mapping from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes from cryptography.hazmat.backends import default_backend import tkinter as tk from tkinter import messagebox, ttk
WL_PATH = "english.txt" SEC_Q = [ "name", "surname", "birth", "country", "gender", "sport", "team", "color", "status", "kids", "email", "lucky_num" ] SEC_L = { "name": "First name:", "surname": "Last name:", "birth": "Birth date (YYYY-MM-DD):", "country": "Country:", "gender": "Gender (Male/Female/Other):", "sport": "Favorite sport:", "team": "Favorite team:", "color": "Favorite color:", "status": "Marital status (Single/Married/Divorced):", "kids": "Number of children (number only):", "email": "Primary email:", "lucky_num": "Lucky number (6+ digits):" }
class Wordlist(tuple): def __init__(self, words: Sequence[str]): super().__init__() self._index_from_word = {w: i for i, w in enumerate(words)}
def index(self, word: str) -> int: return self._index_from_word[word]
def __contains__(self, word: str) -> bool: return word in self._index_from_word
def load_wordlist(): try: with open(WL_PATH, "r", encoding="utf-8") as f: words = [w.strip() for w in f if w.strip()] if len(words) != 2048: raise ValueError("Wordlist must contain exactly 2048 words") return Wordlist(words) except Exception as e: messagebox.showerror("Error", f"Failed to load wordlist: {str(e)}") exit()
WL = load_wordlist()
def electrum_encode(entropy: bytes) -> str: word_counts = {16: 12, 24: 18, 32: 24} if len(entropy) not in word_counts: raise ValueError("Electrum entropy must be 16, 24, or 32 bytes") words_count = word_counts[len(entropy)] bits = bin(int.from_bytes(entropy, 'big'))[2:].zfill(len(entropy) * 8) total_bits = words_count * 11 bits = bits.ljust(total_bits, '0') return ' '.join([WL[int(bits[i:i+11], 2)] for i in range(0, total_bits, 11)])
def electrum_decode(seed: str) -> bytes: words = seed.split() word_bytes = {12: 16, 18: 24, 24: 32} usable_words = {13: 12, 19: 18, 25: 24, 12: 12, 18: 18, 24: 24} if len(words) not in usable_words: raise ValueError("Electrum seed must be 12, 13, 18, 19, 24 or 25 words") usable = usable_words[len(words)] bpw = 11 bits = ''.join([bin(WL.index(w))[2:].zfill(bpw) for w in words[:usable]]) entropy_bytes = word_bytes[usable] entropy_bits = entropy_bytes * 8 bits = bits[:entropy_bits] return int(bits, 2).to_bytes(entropy_bytes, 'big')
def is_electrum_seed(seed: str) -> bool: words = seed.split() return len(words) in [12, 13, 18, 19, 24, 25]
def bip39_checksum(entropy: bytes) -> str: entropy_bits = bin(int.from_bytes(entropy, 'big'))[2:].zfill(len(entropy) * 8) hash_bits = bin(int(hashlib.sha256(entropy).hexdigest(), 16))[2:].zfill(256)[:len(entropy)*8//32] return entropy_bits + hash_bits
def bip39_encode(entropy: bytes) -> str: bits = bip39_checksum(entropy) chunks = [bits[i:i+11] for i in range(0, len(bits), 11)] return ' '.join([WL[int(chunk, 2)] for chunk in chunks])
def bip39_decode(seed: str) -> bytes: words = seed.split() if len(words) not in [12, 15, 18, 21, 24]: raise ValueError("BIP39 seed must be 12, 15, 18, 21 or 24 words") indices = [WL.index(w) for w in words] bits = ''.join([bin(idx)[2:].zfill(11) for idx in indices]) total_bits = len(words) * 11 entropy_bits = total_bits - total_bits // 33 return int(bits[:entropy_bits], 2).to_bytes(entropy_bits // 8, 'big')
def is_bip39_seed(seed: str) -> bool: words = seed.split() if len(words) not in [12, 15, 18, 21, 24]: return False try: bip39_decode(seed) return True except: return False
def seed_to_entropy(seed: str, seed_type: str) -> bytes: if seed_type == "electrum": return electrum_decode(seed) elif seed_type == "bip39": return bip39_decode(seed) else: raise ValueError("Unsupported seed type")
def entropy_to_seed(entropy: bytes, seed_type: str) -> str: if seed_type == "electrum": return electrum_encode(entropy) elif seed_type == "bip39": return bip39_encode(entropy) else: raise ValueError("Unsupported seed type")
def normalize_input(text: str) -> str: return str(text).strip().lower() if text else ""
def validate_answers(answers: Dict[str, str]) -> tuple[bool, str]: if not re.match(r'^\d{6,}$', answers["lucky_num"]): return False, "Lucky number must be 6+ digits" if not re.match(r'^\d{4}-\d{2}-\d{2}$', answers["birth"]): return False, "Birth date must be YYYY-MM-DD" if not re.match(r'^\d+$', answers["kids"]): return False, "Number of children must be numeric" return True, "Valid answers"
def get_salt_and_iv(answers: Dict[str, str]) -> tuple[bytes, bytes]: valid, msg = validate_answers(answers) if not valid: raise ValueError(msg) combined = "".join(normalize_input(answers.get(k, "")) for k in SEC_Q) if not combined: raise ValueError("All security questions must be answered") digest = hashes.Hash(hashes.SHA512(), backend=default_backend()) digest.update(combined.encode('utf-8')) result = digest.finalize() return result[:16], result[16:32]
def derive_key(password: str, salt: bytes) -> bytes: kdf = PBKDF2HMAC( algorithm=hashes.SHA256(), length=32, salt=salt, iterations=60000000, backend=default_backend() ) return kdf.derive(password.encode('utf-8'))
def encrypt_seed(seed: str, answers: Dict[str, str], seed_type: str) -> str: seed_words = seed.strip().split() last_word = None if seed_type == "electrum" and len(seed_words) in [12, 18, 24]: last_word = seed_words[-1] entropy = seed_to_entropy(seed, seed_type) salt, iv = get_salt_and_iv(answers) password = "".join(normalize_input(answers.get(k, "")) for k in SEC_Q) key = derive_key(password, salt) cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend()) encryptor = cipher.encryptor() encrypted = encryptor.update(entropy) + encryptor.finalize() result = entropy_to_seed(encrypted, seed_type) if seed_type == "electrum" and last_word is not None: return result + " " + last_word return result
def decrypt_seed(encrypted_seed: str, answers: Dict[str, str], seed_type: str) -> str: if seed_type == "electrum": words = encrypted_seed.strip().split() usable_words = {13: 12, 19: 18, 25: 24, 12: 12, 18: 18, 24: 24} if len(words) not in usable_words: raise ValueError("Invalid Electrum seed length") if len(words) in [13, 19, 25]: encryptedN = " ".join(words[:len(words)-1]) original_last = words[-1] else: encryptedN = encrypted_seed.strip() original_last = None encrypted_entropy = seed_to_entropy(encryptedN, seed_type) salt, iv = get_salt_and_iv(answers) password = "".join(normalize_input(answers.get(k, "")) for k in SEC_Q) key = derive_key(password, salt) cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend()) decryptor = cipher.decryptor() decrypted = decryptor.update(encrypted_entropy) + decryptor.finalize() result_seed = entropy_to_seed(decrypted, seed_type) if original_last is not None: result_words = result_seed.split() result_words[-1] = original_last return " ".join(result_words) return result_seed else: encrypted_entropy = seed_to_entropy(encrypted_seed, seed_type) salt, iv = get_salt_and_iv(answers) password = "".join(normalize_input(answers.get(k, "")) for k in SEC_Q) key = derive_key(password, salt) cipher = Cipher(algorithms.AES(key), modes.CFB(iv), backend=default_backend()) decryptor = cipher.decryptor() decrypted = decryptor.update(encrypted_entropy) + decryptor.finalize() return entropy_to_seed(decrypted, seed_type)
class SeedApp: def __init__(self, root): self.root = root self.setup_main_window() self.setup_notebook() self.setup_encrypt_tab() self.setup_decrypt_tab()
def setup_main_window(self): self.root.title("Camouflage") self.root.geometry("600x520") self.root.resizable(False, False)
def setup_notebook(self): self.notebook = ttk.Notebook(self.root) self.notebook.pack(padx=10, pady=10, expand=True, fill="both") self.encrypt_frame = ttk.Frame(self.notebook, padding=15) self.notebook.add(self.encrypt_frame, text="Encrypt Seed") self.decrypt_frame = ttk.Frame(self.notebook, padding=15) self.notebook.add(self.decrypt_frame, text="Decrypt Seed")
def setup_context_menu(self, widget): menu = tk.Menu(widget, tearoff=0) menu.add_command(label="Copy", command=lambda: widget.event_generate("<<Copy>>")) menu.add_command(label="Paste", command=lambda: self.safe_paste(widget)) menu.add_command(label="Cut", command=lambda: widget.event_generate("<<Cut>>")) widget.bind("<Button-3>", lambda e: menu.tk_popup(e.x_root, e.y_root)) widget.bind("<Control-c>", lambda e: widget.event_generate("<<Copy>>")) widget.bind("<Control-v>", lambda e: self.safe_paste(widget) or "break") widget.bind("<Control-x>", lambda e: widget.event_generate("<<Cut>>"))
def safe_paste(self, widget): try: widget.delete("sel.first", "sel.last") except tk.TclError: pass widget.insert("insert", self.root.clipboard_get())
def setup_encrypt_tab(self): ttk.Label(self.encrypt_frame, text="Original Seed Phrase:", font=('Arial', 10, 'bold')).pack(anchor="w", pady=5) input_frame = ttk.LabelFrame(self.encrypt_frame, text="Input", padding=5) input_frame.pack(fill="x", padx=5, pady=5) self.seed_input = tk.Text(input_frame, height=5, width=90, wrap="word", borderwidth=1, relief="solid", font=('Arial', 9)) self.seed_input.pack(fill="both", expand=True) self.setup_context_menu(self.seed_input) ttk.Label(self.encrypt_frame, text="Seed Type:", font=('Arial', 10, 'bold')).pack(anchor="w", pady=5) self.encrypt_seed_type = ttk.Combobox(self.encrypt_frame, values=["BIP39", "Electrum"], state="readonly") self.encrypt_seed_type.set("BIP39") self.encrypt_seed_type.pack(anchor="w", padx=5, pady=5) btn_frame = ttk.Frame(self.encrypt_frame) btn_frame.pack(fill="x", padx=5, pady=5) ttk.Button(btn_frame, text="Encrypt", command=self.encrypt).pack(side="left", padx=5) ttk.Label(self.encrypt_frame, text="Encrypted Seed Phrase:", font=('Arial', 10, 'bold')).pack(anchor="w", pady=5) output_frame = ttk.LabelFrame(self.encrypt_frame, text="Output", padding=5) output_frame.pack(fill="x", padx=5, pady=5) self.encrypted_output = tk.Text(output_frame, height=5, width=90, wrap="word", state="disabled", borderwidth=1, relief="solid", font=('Arial', 9)) self.encrypted_output.pack(fill="both", expand=True) self.setup_context_menu(self.encrypted_output) copy_frame = ttk.Frame(self.encrypt_frame) copy_frame.pack(fill="x", padx=5, pady=(0, 10)) ttk.Button(copy_frame, text="Copy to Clipboard", command=self.copy_encrypted).pack(side="right")
def setup_decrypt_tab(self): ttk.Label(self.decrypt_frame, text="Encrypted Seed Phrase:", font=('Arial', 10, 'bold')).pack(anchor="w", pady=5) input_frame = ttk.LabelFrame(self.decrypt_frame, text="Input", padding=5) input_frame.pack(fill="x", padx=5, pady=5) self.encrypted_input = tk.Text(input_frame, height=5, width=90, wrap="word", borderwidth=1, relief="solid", font=('Arial', 9)) self.encrypted_input.pack(fill="both", expand=True) self.setup_context_menu(self.encrypted_input) ttk.Label(self.decrypt_frame, text="Seed Type:", font=('Arial', 10, 'bold')).pack(anchor="w", pady=5) self.decrypt_seed_type = ttk.Combobox(self.decrypt_frame, values=["BIP39", "Electrum"], state="readonly") self.decrypt_seed_type.set("BIP39") self.decrypt_seed_type.pack(anchor="w", padx=5, pady=5) btn_frame = ttk.Frame(self.decrypt_frame) btn_frame.pack(fill="x", padx=5, pady=5) ttk.Button(btn_frame, text="Decrypt", command=self.decrypt).pack(side="left", padx=5) self.output_label = ttk.Label(self.decrypt_frame, text="Decrypted Seed:", font=('Arial', 10, 'bold')) self.output_label.pack(anchor="w", pady=5) self.output_frame = ttk.LabelFrame(self.decrypt_frame, text="Output", padding=5) self.output_frame.pack(fill="x", padx=5, pady=5) self.decrypted_output = tk.Text(self.output_frame, height=5, width=90, wrap="word", state="disabled", borderwidth=1, relief="solid", font=('Arial', 9)) self.decrypted_output.pack(fill="x") copy_frame = ttk.Frame(self.decrypt_frame) copy_frame.pack(fill="x", padx=5, pady=(0, 10)) ttk.Button(copy_frame, text="Copy to Clipboard", command=self.copy_decrypted).pack(side="right")
def encrypt(self): self.encrypted_output.config(state="normal") self.encrypted_output.delete("1.0", tk.END) seed = self.seed_input.get("1.0", tk.END).strip() if not seed: messagebox.showerror("Error", "Please enter a seed phrase") return answers = self.get_security_answers() if not answers: return seed_type = self.encrypt_seed_type.get().lower() try: encrypted = encrypt_seed(seed, answers, seed_type) self.encrypted_output.insert(tk.END, encrypted) messagebox.showinfo("Success", f"Seed encrypted as {seed_type.upper()}") except Exception as e: messagebox.showerror("Error", str(e)) finally: self.encrypted_output.config(state="disabled")
def decrypt(self): self.decrypted_output.config(state="normal") self.decrypted_output.delete("1.0", tk.END) encrypted = self.encrypted_input.get("1.0", tk.END).strip() if not encrypted: messagebox.showerror("Error", "Please enter an encrypted seed") return answers = self.get_security_answers() if not answers: return seed_type = self.decrypt_seed_type.get().lower() try: decrypted = decrypt_seed(encrypted, answers, seed_type) self.decrypted_output.insert(tk.END, decrypted) self.output_label.config(text=f"Decrypted Seed ({seed_type.upper()}):") messagebox.showinfo("Success", f"Decrypted as {seed_type.upper()} seed") except Exception as e: messagebox.showerror("Error", str(e)) finally: self.decrypted_output.config(state="disabled")
def copy_encrypted(self): self.copy_to_clipboard(self.encrypted_output)
def copy_decrypted(self): self.copy_to_clipboard(self.decrypted_output)
def copy_to_clipboard(self, widget): widget.config(state="normal") text = widget.get("1.0", tk.END).strip() widget.config(state="disabled") if text: self.root.clipboard_clear() self.root.clipboard_append(text) messagebox.showinfo("Copied", "Text copied to clipboard")
def get_security_answers(self): dialog = SecurityDialog(self.root) self.root.wait_window(dialog) return dialog.result if dialog.result else None
class SecurityDialog(tk.Toplevel): def __init__(self, parent): super().__init__(parent) self.result = None self.answers = {k: tk.StringVar() for k in SEC_Q} self.setup_window(parent) self.setup_ui()
def setup_window(self, parent): self.title("Security Questions") self.geometry("600x600") self.resizable(False, False) self.transient(parent) self.grab_set()
def setup_ui(self): main_frame = ttk.Frame(self) main_frame.pack(fill="both", expand=True, padx=10, pady=10) canvas = tk.Canvas(main_frame, highlightthickness=0) scrollbar = ttk.Scrollbar(main_frame, orient="vertical", command=canvas.yview) scroll_frame = ttk.Frame(canvas) scroll_frame.bind("<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))) canvas.create_window((0, 0), window=scroll_frame, anchor="nw") canvas.configure(yscrollcommand=scrollbar.set) for i, key in enumerate(SEC_Q): frame = ttk.LabelFrame(scroll_frame, text=SEC_L[key], padding=5) frame.grid(row=i, column=0, padx=5, pady=5, sticky="ew") if key == "birth": ttk.Label(frame, text="Format: YYYY-MM-DD", foreground="gray").pack(anchor="w") elif key == "lucky_num": ttk.Label(frame, text="Must be 6+ digits", foreground="gray").pack(anchor="w") elif key == "kids": ttk.Label(frame, text="Enter 0 if none", foreground="gray").pack(anchor="w") entry = ttk.Entry(frame, textvariable=self.answers[key], width=50) entry.pack(fill="x", padx=5, pady=2) btn_frame = ttk.Frame(main_frame) ttk.Button(btn_frame, text="Submit", command=self.submit).pack(side="left", padx=5) ttk.Button(btn_frame, text="Cancel", command=self.cancel).pack(side="right", padx=5) canvas.pack(side="left", fill="both", expand=True) scrollbar.pack(side="right", fill="y") btn_frame.pack(side="bottom", fill="x", pady=10) self.bind_mousewheel(scroll_frame, canvas) self.center_window()
def bind_mousewheel(self, widget, canvas): def on_scroll(event): if event.delta: canvas.yview_scroll(-1 * (event.delta // 120), "units") elif event.num == 4: canvas.yview_scroll(-1, "units") elif event.num == 5: canvas.yview_scroll(1, "units") widget.bind("<MouseWheel>", on_scroll) widget.bind("<Button-4>", on_scroll) widget.bind("<Button-5>", on_scroll) for child in widget.winfo_children(): self.bind_mousewheel(child, canvas)
def center_window(self): self.update_idletasks() width = self.winfo_width() height = self.winfo_height() x = (self.winfo_screenwidth() // 2) - (width // 2) y = (self.winfo_screenheight() // 2) - (height // 2) self.geometry(f"{width}x{height}+{x}+{y}")
def validate(self): errors = [] if not re.match(r'^\d{6,}$', self.answers["lucky_num"].get()): errors.append("Lucky number must be 6+ digits") if not re.match(r'^\d{4}-\d{2}-\d{2}$', self.answers["birth"].get()): errors.append("Birth date must be YYYY-MM-DD") if not re.match(r'^\d+$', self.answers["kids"].get()): errors.append("Number of children must be numeric") missing = [SEC_L[k] for k, v in self.answers.items() if not v.get().strip()] if missing: errors.append(f"Missing fields: {', '.join(missing)}") return errors
def submit(self): errors = self.validate() if errors: messagebox.showerror("Validation Error", "\n".join(errors)) return self.result = {k: v.get().strip() for k, v in self.answers.items()} self.destroy()
def cancel(self): self.destroy()
if __name__ == "__main__": root = tk.Tk() app = SeedApp(root) root.mainloop()
english.txt
This code is an experiment, don´t use real seeds or rely on this encryption, as its security has not been verified.
|