Skip to content

Commit eaf2f14

Browse files
committed
Add >2012 magic search key
1 parent e58a8e0 commit eaf2f14

File tree

2 files changed

+42
-31
lines changed

2 files changed

+42
-31
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ A tool to recover lost bitcoin private keys from dead hard drives.
77

88
```bash
99
python3 keyhunter.py /dev/sdX
10-
10+
# --or--
1111
./keyhunter.py /dev/sdX
1212
```
1313

1414
The output should list found private keys, in base58 key import format.
1515

16-
To import into bitcoind, use the following command:
16+
To import into bitcoind, use the following command for each key:
1717

1818
```bash
19-
bitcoind importprivkey 5K????????????? yay
19+
bitcoind importprivkey 5KXXXXXXXXXXXX
2020
bitcoind getbalance
2121
```
2222

keyhunter.py

Lines changed: 39 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,19 @@
1414
# bytes to read at a time from file (10 MiB)
1515
READ_BLOCK_SIZE = 10 * 1024 * 1024
1616

17-
MAGIC_BYTES = b"\x01\x30\x82\x01\x13\x02\x01\x01\x04\x20"
18-
MAGIC_BYTES_LEN = len(MAGIC_BYTES)
17+
MAGIC_BYTES_LIST = [
18+
bytes.fromhex("01308201130201010420"), # old, <2012
19+
bytes.fromhex("01d63081d30201010420"), # new, >2012
20+
]
21+
MAGIC_BYTES_LEN = 10 # length of each element in MAGIC_BYTES_LIST
22+
assert all(len(magic_bytes) == MAGIC_BYTES_LEN for magic_bytes in MAGIC_BYTES_LIST)
1923

2024

2125
B58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
2226
B58_BASE = len(B58_CHARS) # literally 58
2327

2428

25-
def b58encode(v):
29+
def b58encode(v: bytes) -> str:
2630
"""encode v, which is a string of bytes, to base58."""
2731

2832
long_value = 0
@@ -47,50 +51,57 @@ def b58encode(v):
4751
return (B58_CHARS[0] * nPad) + result
4852

4953

50-
def Hash(data):
54+
def sha256d_hash(data: bytes) -> bytes:
5155
return hashlib.sha256(hashlib.sha256(data).digest()).digest()
5256

5357

54-
def EncodeBase58Check(secret):
55-
hash = Hash(secret)
58+
def encode_base58_check(secret: bytes) -> str:
59+
hash = sha256d_hash(secret)
5660
return b58encode(secret + hash[0:4])
5761

5862

5963
def find_keys(filename: str | Path) -> set[str]:
64+
"""Searches a file for Bitcoin private keys.
65+
Returns a set of private keys as base58 WIF strings.
66+
"""
67+
6068
keys = set()
6169
with open(filename, "rb") as f:
6270
logger.info(f"Opened file: {filename}")
6371

6472
# read through target file one block at a time
65-
while data := f.read(READ_BLOCK_SIZE):
66-
# look in this block for keys
67-
pos = 0 # index in the block
68-
while (pos := data.find(MAGIC_BYTES, pos)) > -1:
69-
# find the magic number
70-
key_offset = pos + MAGIC_BYTES_LEN
71-
key_data = "\x80" + data[key_offset : key_offset + 32] # noqa: E203
72-
priv_key_wif = EncodeBase58Check(key_data)
73-
keys.add(priv_key_wif)
74-
logger.info(
75-
f"Found key at offset {key_offset:,} = 0x{key_offset:02x}: {priv_key_wif}"
76-
)
77-
pos += 1
78-
79-
# are we at the end of the file?
80-
if len(data) == READ_BLOCK_SIZE:
81-
logger.info("At end of file. Seeking back 32 bytes.")
82-
# make sure we didn't miss any keys at the end of the block
73+
while block_bytes := f.read(READ_BLOCK_SIZE):
74+
# look in this block for each key
75+
for magic_bytes in MAGIC_BYTES_LIST:
76+
pos = 0 # index in the block
77+
while (pos := block_bytes.find(magic_bytes, pos)) > -1:
78+
# find the magic number
79+
key_offset = pos + MAGIC_BYTES_LEN
80+
key_data = b"\x80" + block_bytes[key_offset : key_offset + 32] # noqa: E203
81+
priv_key_wif = encode_base58_check(key_data)
82+
keys.add(priv_key_wif)
83+
logger.info(
84+
f"Found key at offset {key_offset:,} = 0x{key_offset:02x} "
85+
f"(using magic bytes {magic_bytes.hex()}): {priv_key_wif}"
86+
)
87+
pos += 1
88+
89+
# Make sure we didn't miss any keys at the end of the block.
90+
# After scanning the block, seek back so that the next block includes the overlap.
91+
if len(block_bytes) == READ_BLOCK_SIZE:
8392
f.seek(f.tell() - (32 + MAGIC_BYTES_LEN))
93+
94+
logger.info(f"Closed file: {filename}")
8495
return keys
8596

8697

87-
def setup_logging(log_filename: Optional[str | Path] = None):
98+
def setup_logging(log_filename: Optional[str | Path] = None) -> logging.Logger:
8899
# Create a logger object
89100
logger = logging.getLogger()
90101
logger.setLevel(logging.DEBUG) # Set the logging level
91102

92103
# Create a console handler and set level to debug
93-
console_handler = logging.StreamHandler(sys.stdout) # Using stdout instead of stderr
104+
console_handler = logging.StreamHandler(sys.stderr)
94105
console_handler.setLevel(logging.DEBUG)
95106
console_formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
96107
console_handler.setFormatter(console_formatter)
@@ -116,11 +127,11 @@ def main_keyhunter(haystack_filename: str | Path, log_path: Optional[str | Path]
116127
logger.info(f"Found {len(keys)} keys: {keys}")
117128

118129
if len(keys) > 0:
119-
logger.info("Keys (as base58 WIF private keys):")
130+
logger.info("Printing keys (as base58 WIF private keys) for easy copying:")
120131
for key in keys:
121132
print(key)
122133

123-
logger.info("Finished keyhunter")
134+
logger.info(f"Finished keyhunter. Found {len(keys):,} keys.")
124135

125136

126137
def get_args():

0 commit comments

Comments
 (0)