Bitcoin Forum
June 10, 2025, 10:22:45 AM *
News: Latest Bitcoin Core release: 29.0 [Torrent]
 
   Home   Help Search Login Register More  
Pages: [1]
  Print  
Author Topic: Masking seed phrases for an extra layer of security (experimental)  (Read 195 times)
mcdouglasx (OP)
Sr. Member
****
Offline Offline

Activity: 658
Merit: 280


View Profile WWW
June 03, 2025, 06:18:30 PM
Last edit: June 03, 2025, 07:00:27 PM by mcdouglasx
Merited by vapourminer (6), Forsyth Jones (3)
 #1

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

Code:
pip install cryptography


python script

Code:
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.


BTCW
Copper Member
Full Member
***
Offline Offline

Activity: 196
Merit: 272

Click "+Merit" top-right corner


View Profile
June 04, 2025, 12:38:30 PM
Merited by satscraper (5), vapourminer (4)
 #2

Nice coding job.

But I fail to see the point. A private key is exactly 256 bits, or 32 bytes, if you will, and BIP39 seed phrases are just a dictionary for it—human-readable binary. There is nothing magic about the words in the wordlist. It is literally a dictionary, and your suggestion accomplishes what? A dictionary for a dictionary?

BIP38 from 2011 allows for standardized encrypted private keys. If I ever were to encrypt a private key, my immediate thought would be: Should I be dealing with crypto in the first place if I lack the skills to keep 32 bytes of data a secret? If I, for some reason, still decided I wanted to encrypt a private key, which in my opinion accomplishes nothing other than increasing complexity and increasing the risk of losing control (because what if you, two weeks or 12 years later, can't recover the 32-character password you went with), I would use a well-established and war-tested standard for it.

My point is that private keys are safe as they are. Whether you like to save them as a 64-character hexadecimal string or as Base58check is just a matter of taste—both represent the exact same thing.

Unless you have a Ph.D. in cryptography, don't invent your own safety layers; they tend to bite you in the ass sooner or later.

SendBTC.me <<< amazing imitative
mcdouglasx (OP)
Sr. Member
****
Offline Offline

Activity: 658
Merit: 280


View Profile WWW
June 04, 2025, 01:37:15 PM
Merited by vapourminer (2)
 #3

Nice coding job.

But I fail to see the point. A private key is exactly 256 bits, or 32 bytes, if you will, and BIP39 seed phrases are just a dictionary for it—human-readable binary. There is nothing magic about the words in the wordlist. It is literally a dictionary, and your suggestion accomplishes what? A dictionary for a dictionary?

BIP38 from 2011 allows for standardized encrypted private keys. If I ever were to encrypt a private key, my immediate thought would be: Should I be dealing with crypto in the first place if I lack the skills to keep 32 bytes of data a secret? If I, for some reason, still decided I wanted to encrypt a private key, which in my opinion accomplishes nothing other than increasing complexity and increasing the risk of losing control (because what if you, two weeks or 12 years later, can't recover the 32-character password you went with), I would use a well-established and war-tested standard for it.

My point is that private keys are safe as they are. Whether you like to save them as a 64-character hexadecimal string or as Base58check is just a matter of taste—both represent the exact same thing.

Unless you have a Ph.D. in cryptography, don't invent your own safety layers; they tend to bite you in the ass sooner or later.

The idea behind this is simple, a steganographic trick.

Even if an attacker obtains your seed phrase, they wouldn’t know how to access the original funds.

It’s not about encryption itself; it’s about using the encrypted seed phrase as a decoy.

And while you're right in saying that anyone unable to store a key securely shouldn’t be in crypto, that statement is somewhat ambiguous because no one is exempt from being hacked.

Historically, those who have lost the most money due to BTC hacks have all been experts.

for example:

Bitcoin developer @lukedashjr's wallet was hacked

The security of your private keys doesn’t rely solely on encryption, it depends on how you protect that encryption. But it’s true, the best approach is always to use standardized methods, especially for someone lacking technical knowledge.
pooya87
Legendary
*
Offline Offline

Activity: 3836
Merit: 11674



View Profile
June 04, 2025, 01:46:47 PM
 #4

It’s not about encryption itself; it’s about using the encrypted seed phrase as a decoy.
If that's the only objective then the solution already exists and there is no reason to re-invent the wheel. It is called the extra word or the passphrase. It is any phrase that you can add to your existing seed phrase to derive an entirely different set of keys. The seed is still the same and is the decoy itself. The only way to access the funds is to know that passphrase.
That passphrase can be the same password you used to encrypt in your algorithm (birthday, passport number, etc...).

mcdouglasx (OP)
Sr. Member
****
Offline Offline

Activity: 658
Merit: 280


View Profile WWW
June 04, 2025, 02:54:04 PM
Merited by vapourminer (2), pooya87 (2)
 #5

It’s not about encryption itself; it’s about using the encrypted seed phrase as a decoy.
If that's the only objective then the solution already exists and there is no reason to re-invent the wheel. It is called the extra word or the passphrase. It is any phrase that you can add to your existing seed phrase to derive an entirely different set of keys. The seed is still the same and is the decoy itself. The only way to access the funds is to know that passphrase.
That passphrase can be the same password you used to encrypt in your algorithm (birthday, passport number, etc...).

Although both involve data-encryption-secret, they're technically not the same, their structure isn't identical.

BIP39 passwords use 2048 iterations when adding passphrases, but this method increases that to 60 million, making brute-forcing practically impossible.

If I post a BIP39 seed here, no one can determine with certainty whether it's standard, password-protected, or encrypted with this script.

That's what security through obscurity is all about. Hackers follow the usual standard, so if they don't know which path leads to the target, I doubt they'll be able to make progress.

This is how second layers of security should work, in my opinion. It's not just about encryption, but about adding an additional, unpredictable move to confuse potential attackers.
BTCW
Copper Member
Full Member
***
Offline Offline

Activity: 196
Merit: 272

Click "+Merit" top-right corner


View Profile
June 04, 2025, 03:49:18 PM
 #6

It’s not about encryption itself; it’s about using the encrypted seed phrase as a decoy.
If that's the only objective then the solution already exists and there is no reason to re-invent the wheel. It is called the extra word or the passphrase. It is any phrase that you can add to your existing seed phrase to derive an entirely different set of keys. The seed is still the same and is the decoy itself. The only way to access the funds is to know that passphrase.
That passphrase can be the same password you used to encrypt in your algorithm (birthday, passport number, etc...).

Although both involve data-encryption-secret, they're technically not the same, their structure isn't identical.

BIP39 passwords use 2048 iterations when adding passphrases, but this method increases that to 60 million, making brute-forcing practically impossible.

If I post a BIP39 seed here, no one can determine with certainty whether it's standard, password-protected, or encrypted with this script.

That's what security through obscurity is all about. Hackers follow the usual standard, so if they don't know which path leads to the target, I doubt they'll be able to make progress.

This is how second layers of security should work, in my opinion. It's not just about encryption, but about adding an additional, unpredictable move to confuse potential attackers.

I don't think you have understood BIP39. It is not about 2048 "iterations". BIP39 is a human-readable dictionary for a (hopefully) truly random number from 0 to 115792089237316195423570985008687907853269984665640564039457584007913129639936 - that is the private key.

SendBTC.me <<< amazing imitative
mcdouglasx (OP)
Sr. Member
****
Offline Offline

Activity: 658
Merit: 280


View Profile WWW
June 04, 2025, 04:41:37 PM
Merited by Forsyth Jones (2)
 #7

It’s not about encryption itself; it’s about using the encrypted seed phrase as a decoy.
If that's the only objective then the solution already exists and there is no reason to re-invent the wheel. It is called the extra word or the passphrase. It is any phrase that you can add to your existing seed phrase to derive an entirely different set of keys. The seed is still the same and is the decoy itself. The only way to access the funds is to know that passphrase.
That passphrase can be the same password you used to encrypt in your algorithm (birthday, passport number, etc...).

Although both involve data-encryption-secret, they're technically not the same, their structure isn't identical.

BIP39 passwords use 2048 iterations when adding passphrases, but this method increases that to 60 million, making brute-forcing practically impossible.

If I post a BIP39 seed here, no one can determine with certainty whether it's standard, password-protected, or encrypted with this script.

That's what security through obscurity is all about. Hackers follow the usual standard, so if they don't know which path leads to the target, I doubt they'll be able to make progress.

This is how second layers of security should work, in my opinion. It's not just about encryption, but about adding an additional, unpredictable move to confuse potential attackers.

I don't think you have understood BIP39. It is not about 2048 "iterations". BIP39 is a human-readable dictionary for a (hopefully) truly random number from 0 to 115792089237316195423570985008687907853269984665640564039457584007913129639936 - that is the private key.

I've made enough posts here to prove that I know how bip39 works, you're just taking everything out of context, because yeah, BIP39 itself is a system for converting random numbers into human-readable phrases, but the master key is not extracted directly from the seed phrase.

It goes through PBKDF2 with 2048 iterations to derive the key used in HD wallets(minimizing BF attack).

So yes, BIP39 defines the structure of the phrases, but the number of iterations is still relevant to the security of the derivation process, and when you include a passphrase, it's used as an additional layer of security, which is what's being discussed here, we're not discussing how entropy is represented in words in a basic approach, we're discussing how a second layer of security with a proportionally higher number of iterations and a non-standard structure that hinders the ability to brute force attack.

It's not just about how entropy is represented, but how actual protection is implemented.
BTCW
Copper Member
Full Member
***
Offline Offline

Activity: 196
Merit: 272

Click "+Merit" top-right corner


View Profile
June 04, 2025, 06:59:48 PM
 #8

Suppose you have a random number generator that produces your private key, as all serious wallet software and hardware have. In that case, you lower the entropy by including known factors such as birthdates and passport numbers.

SendBTC.me <<< amazing imitative
mcdouglasx (OP)
Sr. Member
****
Offline Offline

Activity: 658
Merit: 280


View Profile WWW
June 04, 2025, 07:40:30 PM
 #9

Suppose you have a random number generator that produces your private key, as all serious wallet software and hardware have. In that case, you lower the entropy by including known factors such as birthdates and passport numbers.

I'm trying to understand your point, but I don't see it.

I suppose you're assuming a case similar to that of brainwallets. If so, you're mixing different concepts.

Here, the initial entropy of the seed remains the same because by encrypting, you're not modifying the original source of randomness.

Or maybe you mean that it would be easier to guess, but it is not like that, because the attacker faces several challenges:

1, Find the encrypted seed.

2 Guess that an additional layer of security was used.

3 Guess which method was used.

4 And last but not least, in an extreme case that knows all the above, he will have to brute force, at 60 million iterations per attempt, which makes it extremely demanding.
Forsyth Jones
Legendary
*
Offline Offline

Activity: 1554
Merit: 1424


I love Bitcoin!


View Profile WWW
June 04, 2025, 09:57:46 PM
 #10

It’s not about encryption itself; it’s about using the encrypted seed phrase as a decoy.
If that's the only objective then the solution already exists and there is no reason to re-invent the wheel. It is called the extra word or the passphrase. It is any phrase that you can add to your existing seed phrase to derive an entirely different set of keys. The seed is still the same and is the decoy itself. The only way to access the funds is to know that passphrase.
That passphrase can be the same password you used to encrypt in your algorithm (birthday, passport number, etc...).
Many might argue that a BIP39 passphrase eliminates any need for encryption and/or steganography techniques of an original BIP39 mnemonic, you may be right, there is no right or wrong, simply each person adopts a cryptographic solution that meets their threat model.

There are several methods to generate a decoy seed like the OP's method, Seed-OTP, BIP-85 can also be used, although its original proposal isn't for that, anyway, it's just another extra layer of security that makes backups more resilient, but the risk of forgetting or confusion increases.... that is why it's important to document everything and review the backups from time to time.

I haven't tested the code yet, I hope to test it today or in a few days.

~Remembering that the passphrase is very secure and should work for 99% of users, in addition to being a standardized method, known and supported by most wallets.

pooya87
Legendary
*
Offline Offline

Activity: 3836
Merit: 11674



View Profile
June 05, 2025, 05:12:02 AM
Merited by mcdouglasx (1)
 #11

It’s not about encryption itself; it’s about using the encrypted seed phrase as a decoy.
If that's the only objective then the solution already exists and there is no reason to re-invent the wheel. It is called the extra word or the passphrase. It is any phrase that you can add to your existing seed phrase to derive an entirely different set of keys. The seed is still the same and is the decoy itself. The only way to access the funds is to know that passphrase.
That passphrase can be the same password you used to encrypt in your algorithm (birthday, passport number, etc...).
Many might argue that a BIP39 passphrase eliminates any need for encryption and/or steganography techniques of an original BIP39 mnemonic, you may be right, there is no right or wrong, simply each person adopts a cryptographic solution that meets their threat model.
The BIP39 passphrase cannot be a replacement for encryption and even though it adds an extra layer of protection but it is not meant for encryption. It is suitable to get that "decoy" which is why I suggested it based on what OP said about the objective.

But OP's method (if I understood the code correctly) is actually encrypting the mnemonic since under the hood it is using AES to encrypt the entropy. Although looking at the code again, I'd drop PBKDF2 and use scrypt instead (like BIP38 does) since scrypt is strong by design and there is no need to use such high iterations as 60 million.

Also I should point out the checksum for Electrum mnemonics (unlike BIP39) is not in the last word to keep it when you want the result to remain valid, it is the entire seed that is hashed to get that checksum. This is why the Electrum's actual entropy size is 132 bits not 128 for 12 words. So OP's idea won't work for Electrum mnemonics unless a brute force tactic is added but even that will have a flaw. If interested, I can explain more.

satscraper
Legendary
*
Offline Offline

Activity: 1120
Merit: 1909



View Profile
June 05, 2025, 02:53:02 PM
 #12


 I fail to see the point.

Unless you have a Ph.D. in cryptography, don't invent your own safety layers; they tend to bite you in the ass sooner or later.

Agreed. No sense to spend the time reinventing the wheel. PGP encryption has been around for over two decades and is the proven and robust scheme. When applied to SEED phrases, it renders them indistinguishable from any other encrypted messages, which is ideal for masking purposes. I can't think of more effective or widely trusted method for this use case. BTW, relevant private PGP key can be securely cloned onto multiple hardware dongles.Wink


 

▄███████████████████▄
████████████████████████

██████████▀▀▀▀██████████
███████████████▀▀███████
█████████▄▄███▄▄█████
████████▀▀████▀███████
█████████▄▄██▀██████████
████████████▄███████████
██████████████▄█████████
██████████▀▀███▀▀███████
███████████████████████
█████████▄▄████▄▄████████
▀███████████████████▀
.
 BC.GAME 
███████████████
███████████████
███████████████
███████████████
██████▀░▀██████
████▀░░░░░▀████
███░░░░░░░░░███
███▄░░▄░▄░░▄███
█████▀░░░▀█████

███████████████

███████████████

███████████████

███████████████
███████████████
███████████████
███████████████
███████████████
███░░▀░░░▀░░███
███░░▄▄▄░░▄████
███▄▄█▀░░▄█████
█████▀░░▐██████
█████░░░░██████

███████████████

███████████████

███████████████

███████████████
███████████████
███████████████
███████████████
███████████████
██████▀▀░▀▄░███
████▀░░▄░▄░▀███
███▀░░▀▄▀▄░▄███
███▄░░▀░▀░▄████
███░▀▄░▄▄██████

███████████████

███████████████

███████████████

███████████████

DEPOSIT BONUS
.1000%.
GET FREE
...5 BTC...

REFER & EARN
..$1000 + 15%..
COMMISSION


 Play Now 
mcdouglasx (OP)
Sr. Member
****
Offline Offline

Activity: 658
Merit: 280


View Profile WWW
June 05, 2025, 03:17:06 PM
 #13


 I fail to see the point.

Unless you have a Ph.D. in cryptography, don't invent your own safety layers; they tend to bite you in the ass sooner or later.

Agreed. No sense to spend the time reinventing the wheel. PGP encryption has been around for over two decades and is the proven and robust scheme. When applied to SEED phrases, it renders them indistinguishable from any other encrypted message, which is ideal for masking purposes. I can't think of more effective or widely trusted method for this use case. BTW, relevant private PGP key can be securely cloned onto multiple hardware dongles.Wink

And yet, PGP has been compromised countless times in the past, causing huge monetary losses and forcing users to leave the forum.

To put it in context, this isn't about which encryption algorithm is better; it's about exploring ways to camouflage the seed in unconventional (custom) ways that make it impossible for an attacker to derive it.

This isn't about PGP; we're tired of seeing these types of keys repeatedly compromised in the past because they're a cryptographic standard.

The main idea is to demonstrate that a person can customize their own security to a point that makes hacking impossible, even with partial data leaked.
satscraper
Legendary
*
Offline Offline

Activity: 1120
Merit: 1909



View Profile
June 05, 2025, 04:19:17 PM
Last edit: June 05, 2025, 04:38:44 PM by satscraper
 #14


 I fail to see the point.

Unless you have a Ph.D. in cryptography, don't invent your own safety layers; they tend to bite you in the ass sooner or later.

Agreed. No sense to spend the time reinventing the wheel. PGP encryption has been around for over two decades and is the proven and robust scheme. When applied to SEED phrases, it renders them indistinguishable from any other encrypted message, which is ideal for masking purposes. I can't think of more effective or widely trusted method for this use case. BTW, relevant private PGP key can be securely cloned onto multiple hardware dongles.Wink

And yet, PGP has been compromised countless times in the past, causing huge monetary losses and forcing users to leave the forum.


That was largely due to poor key management, human errors and some flawed implementation choices such as using weaker algorithms or shorter keys. When implemented properly and with modern cryptographic primitives, PGP itself has never been fundamentally compromised. That said, go ahead, you are free "to invent the wheel" to use it for SEED phrase masking. But be aware that your method to mask SEED  doesn’t obscure the presence of BTC or any other crypto behind it while using PGP secures this.

▄███████████████████▄
████████████████████████

██████████▀▀▀▀██████████
███████████████▀▀███████
█████████▄▄███▄▄█████
████████▀▀████▀███████
█████████▄▄██▀██████████
████████████▄███████████
██████████████▄█████████
██████████▀▀███▀▀███████
███████████████████████
█████████▄▄████▄▄████████
▀███████████████████▀
.
 BC.GAME 
███████████████
███████████████
███████████████
███████████████
██████▀░▀██████
████▀░░░░░▀████
███░░░░░░░░░███
███▄░░▄░▄░░▄███
█████▀░░░▀█████

███████████████

███████████████

███████████████

███████████████
███████████████
███████████████
███████████████
███████████████
███░░▀░░░▀░░███
███░░▄▄▄░░▄████
███▄▄█▀░░▄█████
█████▀░░▐██████
█████░░░░██████

███████████████

███████████████

███████████████

███████████████
███████████████
███████████████
███████████████
███████████████
██████▀▀░▀▄░███
████▀░░▄░▄░▀███
███▀░░▀▄▀▄░▄███
███▄░░▀░▀░▄████
███░▀▄░▄▄██████

███████████████

███████████████

███████████████

███████████████

DEPOSIT BONUS
.1000%.
GET FREE
...5 BTC...

REFER & EARN
..$1000 + 15%..
COMMISSION


 Play Now 
mcdouglasx (OP)
Sr. Member
****
Offline Offline

Activity: 658
Merit: 280


View Profile WWW
June 05, 2025, 04:50:13 PM
 #15

That was largely due to poor key management, human errors and some flawed implementation choices such as using weaker algorithms or shorter keys. When implemented properly and with modern cryptographic primitives, PGP itself has never been fundamentally compromised. That said, go ahead, you are free "to invent the wheel" to use it for SEED phrase masking.

You've just discovered the essence of the post, which is that even if this data (the seeds) is leaked, security is not lost because the leak due to hacking was partial.

I don't think it's necessary to explain to you what security through obscurity means.



I don't understand what sense it makes for you to go to a purely experimental thread and publish what we all already know or should know.

Your comment is the equivalent of going to a philosophical debate thread about the existence of God and saying "God doesn't exist" without going into the purely philosophical aspect.
satscraper
Legendary
*
Offline Offline

Activity: 1120
Merit: 1909



View Profile
June 05, 2025, 05:16:31 PM
 #16



I don't understand what sense it makes for you to go to a purely experimental thread and publish what we all already know or should know.
.

Because some users following your "experiment" might get the false sense of security thinking they're safe just because they use the "obscured SEED" you've introduced. But if "$5 wrench" will have the option to glance on yours so called obscured SEED then that illusion of safety will disappear real fast. There’s no actual obscurity involved in your method.

▄███████████████████▄
████████████████████████

██████████▀▀▀▀██████████
███████████████▀▀███████
█████████▄▄███▄▄█████
████████▀▀████▀███████
█████████▄▄██▀██████████
████████████▄███████████
██████████████▄█████████
██████████▀▀███▀▀███████
███████████████████████
█████████▄▄████▄▄████████
▀███████████████████▀
.
 BC.GAME 
███████████████
███████████████
███████████████
███████████████
██████▀░▀██████
████▀░░░░░▀████
███░░░░░░░░░███
███▄░░▄░▄░░▄███
█████▀░░░▀█████

███████████████

███████████████

███████████████

███████████████
███████████████
███████████████
███████████████
███████████████
███░░▀░░░▀░░███
███░░▄▄▄░░▄████
███▄▄█▀░░▄█████
█████▀░░▐██████
█████░░░░██████

███████████████

███████████████

███████████████

███████████████
███████████████
███████████████
███████████████
███████████████
██████▀▀░▀▄░███
████▀░░▄░▄░▀███
███▀░░▀▄▀▄░▄███
███▄░░▀░▀░▄████
███░▀▄░▄▄██████

███████████████

███████████████

███████████████

███████████████

DEPOSIT BONUS
.1000%.
GET FREE
...5 BTC...

REFER & EARN
..$1000 + 15%..
COMMISSION


 Play Now 
mcdouglasx (OP)
Sr. Member
****
Offline Offline

Activity: 658
Merit: 280


View Profile WWW
June 05, 2025, 05:38:45 PM
 #17



I don't understand what sense it makes for you to go to a purely experimental thread and publish what we all already know or should know.
.

Because some users following your "experiment" might get the false sense of security thinking they're safe just because they use the "obscured SEED" you've introduced. But if "$5 wrench" will have the option to glance on yours so called obscured SEED then that illusion of safety will disappear real fast. There’s no actual obscurity involved in your method.

No, because whenever I do these things I make it clear that I do not recommend using them in real environments.

I suppose you haven't read it (although it is in red and also in the thread title) and that's why this comment.
Forsyth Jones
Legendary
*
Offline Offline

Activity: 1554
Merit: 1424


I love Bitcoin!


View Profile WWW
June 05, 2025, 06:36:11 PM
 #18

Agreed. No sense to spend the time reinventing the wheel. PGP encryption has been around for over two decades and is the proven and robust scheme. When applied to SEED phrases, it renders them indistinguishable from any other encrypted messages, which is ideal for masking purposes. I can't think of more effective or widely trusted method for this use case. BTW, relevant private PGP key can be securely cloned onto multiple hardware dongles.Wink
PGP/GPG is a great alternative for encrypting not only seed phrases, but any type of content; I use it myself from time to time.

The BIP39 passphrase cannot be a replacement for encryption and even though it adds an extra layer of protection but it is not meant for encryption. It is suitable to get that "decoy" which is why I suggested it based on what OP said about the objective.

But OP's method (if I understood the code correctly) is actually encrypting the mnemonic since under the hood it is using AES to encrypt the entropy. Although looking at the code again, I'd drop PBKDF2 and use scrypt instead (like BIP38 does) since scrypt is strong by design and there is no need to use such high iterations as 60 million.

Also I should point out the checksum for Electrum mnemonics (unlike BIP39) is not in the last word to keep it when you want the result to remain valid, it is the entire seed that is hashed to get that checksum. This is why the Electrum's actual entropy size is 132 bits not 128 for 12 words. So OP's idea won't work for Electrum mnemonics unless a brute force tactic is added but even that will have a flaw. If interested, I can explain more.
One of the applicable examples, with a decoy seed encrypted by seed-otp, seed-xor or the method developed by the OP, even if someone discovers the secret of the decoy seed (which is quite unlikely, unless the user was careless at some stage, such as leaving traces of a second multifactorial factor, such as an OTP-key or any method-dependent keys), if the funds are in a hidden wallet accessible only by BIP39 passphrases, he will have to rework the secret again, which will depend solely and exclusively on the strength of the passphrase, which depending on the passphrase used, becomes impossible, but to do so, he would still have to break the xor encryption of the decoy seed.

This is just one use case I can imagine in a scenario where the user is using multifactorial backup as a decoy seed. Which is n't popular nowadays (and this is actually good, as it reduces the attack surface both physically and online), however, since the support and knowledge of the method is less, the user will have to bear the responsibility of keeping the method documented to follow in an eventual recovery, as there's not enough widespread knowledge because it isn't as standardized as PGP, BIP39, etc.

Pages: [1]
  Print  
 
Jump to:  

Powered by MySQL Powered by PHP Powered by SMF 1.1.19 | SMF © 2006-2009, Simple Machines Valid XHTML 1.0! Valid CSS!