1414# bytes to read at a time from file (10 MiB)
1515READ_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
2125B58_CHARS = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
2226B58_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
5963def 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
126137def get_args ():
0 commit comments