Seed Phrase Recovery Tool: Find a Used Seed Phrase with Multiple Missing Words (with Code!)

Seed Phrase Recovery Tool: Find a Used Seed Phrase with Multiple Missing Words (with Code!)

This is the fifth part of the Find My Phrase series in finding a seed phrase (with code!):

We're now able to get a list of potential seed phrases when one or more word(s) are missing (from Part 4).

AND we're also able to check if there is a seed phrase that has been used in a list of seed phrases (from Part 3).

So now we can put these two together...to find a used seed phrase from a list of potential seed phrases when one or more words are missing.

Remember, there is a limit to how many words you can have missing (in terms of seed phrases you can realistically check).

The more words you have missing, the more time and computing power required.

But if you're missing one or two words, you'll be able to find your seed phrase if you used it (i.e. sent or received BTC at some point in time). 

Disclaimer: This is meant to be an educational exercise to utilize programming to explore automation. It is not recommend to do this with your own seed phrase without a secure machine. Entering your seed phrase on a device connected to the internet exposes your seed phrase to potential security threats. If you choose to do so, you fully understand the risks are liable for the consequences.

Finding a Used Seed Phrase with Missing Multiple Words

Step 1: Level Set

You should have three files in your folder now:

find my phrase

Step 2: Importing Phrase Check

Open findmyphrase.py.

We're going to import our phrase checking function by adding import phrase_check to where we import our libraries.

It should look like this at the beginning.

import itertools
import hashlib
import phrase_check

And thats it. We're done.

Putting it Together

As a reminder your two .py files should look like this now:

findmyphrase.py

import itertools
import hashlib
import phrase_check

def get_possible(seed_phrase):

    #converts seed phrase into a list to be able to interface with each word individually.
    seed_phrase = seed_phrase.split(" ")

    if len(seed_phrase) not in [12, 15, 18, 21, 24]:
        print("Your seed phrase must be 12, 15, 18, 21, or 24 words. Please place a question mark (?) for missing words.")
        raise SystemExit(0)

    #opens the "english.txt" file and stores it into variable "english"
    english = open("english.txt")

    #reads the "english.txt" file stored in variable "english" and stores the words in the variable "word_list". Also, changes the variable type to a list.
    word_list = english.read().split("\n")

    #closes the "english.txt" file stored in variable "english" since we don't need it anymore.
    english.close()

    #converts seed_phrase (with words) to indexed number in BIP39 wordlist
    seed_phrase_index = [word_list.index(word) if word != "?" else word for word in seed_phrase]

    #converts seed_phrase_index (with numbers) to binary
    seed_phrase_binary = [format(number, "011b") if number != "?" else number for number in seed_phrase_index]

    #calculates the number of missing bits based on length of seed phrase
    num_missing_bits = int(11-(1/3)*(len(seed_phrase)))

    #calculates all the possible bits for a missing word
    possible_word_bits = (bin(x)[2:].rjust(11, "0") for x in range(2**11))

    if seed_phrase_binary[-1] != "?": #if the last word is not "?"
        missing_bits_possible = (seed_phrase_binary[-1][0:num_missing_bits],) #save the leftover bits
        checksum = seed_phrase_binary[-1][-(11-num_missing_bits):] #save the checksum
    else:
       #calculates all the possible permutation of missing bits for entropy
        missing_bits_possible = (bin(x)[2:].rjust(num_missing_bits, "0") for x in range(2**num_missing_bits)) # calculate all the possible leftover bits
        checksum = "" #empty checksum

    #determine all the possible bit "combinations" (cartesian product) depending on the number of missing words
    possible_word_bits_combination = (combination for combination in itertools.product(possible_word_bits,repeat=seed_phrase[:-1].count("?")))

    #input all the "combinations" into the the binary form of the seed phrase (minus the last word), also known as your partial entropy
    partial_entropy = tuple("".join((combination.pop(0) if word == "?" else seed_phrase_local[index] for index,word in enumerate(seed_phrase_local))) if (seed_phrase_local := seed_phrase_binary[:-1]) and (combination := list(word_bits_combination)) else "".join(seed_phrase_local) for word_bits_combination in possible_word_bits_combination)

    #adds the missing bits to complete the entropy
    entropy_possible = tuple(bit_combination + missing_bits for missing_bits in missing_bits_possible for bit_combination in partial_entropy )

    #input each entropy_possible in the SHA256 function to result in the corresponding checksum
    seed_phrase_binary_possible = (entropy + calc_checksum for entropy in entropy_possible if checksum == (calc_checksum := format(hashlib.sha256(int(entropy, 2).to_bytes(len(entropy) // 8, byteorder="big")).digest()[0],"08b")[:11-num_missing_bits]) or checksum == "")

    #transforms all of the seed phrases in binary form back to word form
    seed_phrase_possible = tuple(" ".join([word_list[int(binary[i:i+11],2)] for i in range(0, len(binary), 11)]) for binary in seed_phrase_binary_possible)
    
    return seed_phrase_possible

phrase_check.py

from pycoin.symbols.btc import network
import requests
import hashlib

def calc_key(seed_phrase , passphrase):
    seed = hashlib.pbkdf2_hmac("sha512",
                                         seed_phrase.encode("utf-8"),
                                         salt=("mnemonic" + passphrase).encode("utf-8"),
                                         iterations=2048,
                                         dklen=64)

    master_key = network.keys.bip32_seed(seed)

    return master_key

def gen_address(derivation_path, master_key):

    subkey = master_key.subkey_for_path(derivation_path)

    hash_160 = subkey.hash160(is_compressed=True)

    if derivation_path[:2] == "49":
        script = network.contract.for_p2pkh_wit(hash_160)
        address = network.address.for_p2s(script)
    elif derivation_path[:2] == "84":
        address = network.address.for_p2pkh_wit(hash_160)
    else:
        address = subkey.address()

    return address

def address_usage(address_list):

    address_url = "https://blockchain.info/balance?active="+"|".join(map("|".join, address_list))

    address_data = requests.get(address_url)
    
    for key,value in address_data.json().items():
        if value['total_received'] > 0:
           address = key
           print("Address: "+ key)
           print("Final Balance: " + str(value["final_balance"]))
           print("Total Recieved: " + str(value["total_received"]))
           print("Number of Tx: " + str(value["n_tx"]))
           break
        else:
            address = ""

    return address
    
def phrase_usage(seed_phrase_list,
                        passphrase = "",
                        derivation_path = ("0/0",
                                                        "44'/0'/0'/0/0",
                                                        "49'/0'/0'/0/0",
                                                        "84'/0'/0'/0/0")):
    max_address_limit = 100

    seed_phrase_limit = max_address_limit//len(derivation_path)

    for i in range(0, len(seed_phrase_list), seed_phrase_limit):
        
        master_keys = [calc_key(seed_phrase, passphrase) for seed_phrase in seed_phrase_list[i:i+seed_phrase_limit]]
        
        addresses = [[gen_address(path, key) for path in derivation_path] for key in master_keys]
        
        address_found = address_usage(addresses)
        
        if address_found != "":
                index_match = [i for i, group in enumerate(addresses)if address_found in group][0]
                print("Seed Phrase: " + seed_phrase_list[i:i+seed_phrase_limit][index_match])
                break

    print("COMPLETE")

Step 3: Testing it Out

We're going to test out with this seed phrase: "? ? sniff tired miracle solve shadow scatter hello never tank side sight isolate sister uniform advice pen praise soap lizard festival connect baby".

Two words are missing now: the first and the second.

Add this to your code:

seed_phrase = "? ? sniff tired miracle solve shadow scatter hello never tank side sight isolate sister uniform advice pen praise soap lizard festival connect baby"

phrase_check.phrase_usage(get_possible(seed_phrase))

Save the code and run it. It might take a few minutes. It may look like nothing is happening for 3-4 minutes but its working.

You should get this result:

Address: 16RCf8jAfz495wTq7umNS8mEv3uNofn6gX
Final Balance: 0
Total Recieved: 8586
Number of Tx: 6
Seed Phrase: element entire sniff tired miracle solve shadow scatter hello never tank side sight isolate sister uniform advice pen praise soap lizard festival connect baby
COMPLETE

I would not try this with more than 2 words at the moment. It'll take a long while and potentially stall your machine.

Back to blog