├── .gitignore ├── walletool ├── __init__.py ├── __pycache__ │ ├── __init__.cpython-314.pyc │ └── init_env.cpython-314.pyc ├── consts.py ├── wallet_files.py ├── bc_data_stream.py ├── init_env.py ├── utils.py └── wallet_items.py ├── testdata ├── btc.txt ├── ltc.txt └── README.md ├── .github └── workflows │ └── ruff.yml ├── README.md ├── check_dogechain.py ├── check_bchain.py └── wt_extract_keys.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.dat 2 | *.py[cod] 3 | .coverage 4 | .idea 5 | htmlcov 6 | -------------------------------------------------------------------------------- /walletool/__init__.py: -------------------------------------------------------------------------------- 1 | # -- encoding: UTF-8 -- 2 | from . import init_env 3 | -------------------------------------------------------------------------------- /testdata/btc.txt: -------------------------------------------------------------------------------- 1 | 1MmsSmihQ9QbVzR6p8e6qvkaFrGzkBGMCJ L3Ha4x43eyLWJxgcLwTc6NsDV5VWypB7uz8YV48pBtSMDTreSsi6 2 | -------------------------------------------------------------------------------- /testdata/ltc.txt: -------------------------------------------------------------------------------- 1 | LbCt2ihPfKWt1MuuyRQuDXLRLEnSSa1hh1 6vtmjqJjeAx3n2Yk27GoqNiFaK8Sk81oqecWEuKnEKZhX7c5rFw 2 | -------------------------------------------------------------------------------- /testdata/README.md: -------------------------------------------------------------------------------- 1 | This directory contains some sanity-checking data. 2 | 3 | Please don't send currency into the addresses herein... 4 | -------------------------------------------------------------------------------- /walletool/__pycache__/__init__.cpython-314.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s4larie/walletool/HEAD/walletool/__pycache__/__init__.cpython-314.pyc -------------------------------------------------------------------------------- /walletool/__pycache__/init_env.cpython-314.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/s4larie/walletool/HEAD/walletool/__pycache__/init_env.cpython-314.pyc -------------------------------------------------------------------------------- /walletool/consts.py: -------------------------------------------------------------------------------- 1 | addrtypes = { 2 | 'bitcoin': 0, 3 | 'litecoin': 48, 4 | 'namecoin': 52, 5 | 'bitcoin-testnet': 111, 6 | 'primecoin': 23, 7 | 'dogecoin': 30, 8 | 'dash': 76, 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/ruff.yml: -------------------------------------------------------------------------------- 1 | # https://beta.ruff.rs 2 | name: ruff 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | jobs: 11 | ruff: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - run: pip install --user ruff 16 | - run: ruff --format=github --ignore=E402,E722,F401 --line-length=171 --target-version=py37 . 17 | -------------------------------------------------------------------------------- /walletool/wallet_files.py: -------------------------------------------------------------------------------- 1 | # -- encoding: UTF-8 -- 2 | import collections 3 | import os 4 | 5 | 6 | def read_wallet_dat(filename): 7 | from bsddb3 import db 8 | filename = os.path.realpath(filename) 9 | env = db.DBEnv() 10 | env.set_lk_detect(db.DB_LOCK_DEFAULT) 11 | env.open( 12 | os.path.dirname(filename), 13 | db.DB_PRIVATE | db.DB_THREAD | db.DB_INIT_LOCK | db.DB_INIT_MPOOL | db.DB_CREATE, 14 | ) 15 | d = db.DB(env) 16 | d.open(filename, 'main', db.DB_BTREE, db.DB_THREAD | db.DB_RDONLY) 17 | return collections.OrderedDict((k, d[k]) for k in d.keys()) 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | walletool ~ a tool for reading wallet.dat files 2 | =============================================== 3 | 4 | A utility for extracting cryptocurrency wallet data from wallet.dat files. 5 | 6 | Installation 7 | ------------ 8 | 9 | * Install Python 3.x. 10 | * Install the `bsddb3` module (if you're on Windows, use Gohlke's site). 11 | 12 | Extracting private keys from Bitcoin-QT/Litecoin-QT wallets 13 | ----------------------------------------------------------- 14 | 15 | * Have your `wallet.dat` handy. 16 | * For Bitcoin, run `python wt_extract_keys.py -d wallet.dat -v 0` 17 | * For Litecoin, run `python wt_extract_keys.py -d wallet.dat -v 48` 18 | 19 | A list of addresses / private keys is printed. 20 | 21 | YMMV :) 22 | -------------------------------------------------------------------------------- /check_dogechain.py: -------------------------------------------------------------------------------- 1 | from walletool import init_env 2 | import argparse 3 | import json 4 | import requests 5 | import sys 6 | import time 7 | 8 | def main(): 9 | ap = argparse.ArgumentParser() 10 | ap.add_argument('file', help='address file; one address per line') 11 | ap.add_argument('--ignore-empty', action='store_true') 12 | args = ap.parse_args() 13 | for line in open(args.file): 14 | line = line.strip() 15 | while True: 16 | r = requests.get('https://dogechain.info/api/v1/address/balance/%s' % line) 17 | if r.status_code == 429: # Too Many Requests 18 | print('Throttled, hold on...', file=sys.stderr) 19 | time.sleep(60) 20 | continue 21 | break 22 | r.raise_for_status() 23 | r = r.json() 24 | if args.ignore_empty and float(r['balance']) == 0: 25 | continue 26 | r['addr'] = line 27 | print(r) 28 | time.sleep(0.5) 29 | 30 | if __name__ == '__main__': 31 | main() 32 | -------------------------------------------------------------------------------- /check_bchain.py: -------------------------------------------------------------------------------- 1 | from walletool import init_env 2 | import json 3 | import re 4 | import requests 5 | import argparse 6 | 7 | 8 | var_re = re.compile('var (.+?) = (.+?);') 9 | 10 | def main(): 11 | ap = argparse.ArgumentParser() 12 | ap.add_argument('file', help='address file; one address per line') 13 | ap.add_argument('--coin', required=True, help='e.g. XPM') 14 | ap.add_argument('--ignore-no-tx', action='store_true') 15 | args = ap.parse_args() 16 | for line in open(args.file): 17 | line = line.strip() 18 | r = requests.get('https://bchain.info/%s/addr/%s' % (args.coin, line)) 19 | if r.status_code == 404: 20 | continue 21 | vs = {} 22 | for m in var_re.finditer(r.text): 23 | key, value = m.groups() 24 | if key == 'startTime': 25 | continue 26 | try: 27 | value = json.loads(value.replace('\'', '"')) 28 | except json.JSONDecodeError: 29 | pass 30 | vs[key] = value 31 | if args.ignore_no_tx and vs['total_tx'] == 0: 32 | continue 33 | print(vs) 34 | 35 | if __name__ == '__main__': 36 | main() 37 | -------------------------------------------------------------------------------- /wt_extract_keys.py: -------------------------------------------------------------------------------- 1 | from walletool import init_env 2 | from walletool.wallet_files import read_wallet_dat 3 | from walletool.wallet_items import parse_wallet_dict, KeyWalletItem 4 | from walletool.consts import addrtypes 5 | import argparse 6 | 7 | def main(): 8 | ap = argparse.ArgumentParser() 9 | ap.add_argument('-d', '--dat', help='wallet.dat path', required=True, dest='filename') 10 | ap.add_argument('-v', '--version', help='address version, as integer, 0xHEX, or any of the following known coins:\n[%s]' % ', '.join(sorted(addrtypes)), required=True) 11 | args = ap.parse_args() 12 | if args.version.startswith('0x'): 13 | version = int(args.version[2:], 16) 14 | elif args.version.isdigit(): 15 | version = int(args.version) 16 | else: 17 | if args.version not in addrtypes: 18 | raise ValueError('invalid version (see --help)') 19 | version = addrtypes[args.version] 20 | w_data = read_wallet_dat(args.filename) 21 | addr_tuples = [] 22 | for item in parse_wallet_dict(w_data): 23 | if isinstance(item, KeyWalletItem): 24 | address = item.get_address(version=version) 25 | privkey = item.get_private_key(version=version) 26 | addr_tuples.append((address, privkey)) 27 | for address, privkey in addr_tuples: 28 | print(address, privkey) 29 | 30 | 31 | if __name__ == '__main__': 32 | main() 33 | -------------------------------------------------------------------------------- /walletool/bc_data_stream.py: -------------------------------------------------------------------------------- 1 | # -- encoding: UTF-8 -- 2 | import sys 3 | 4 | assert sys.version_info[0] == 3 # TODO: Use six for 2/3 compat 5 | 6 | # From Joric's pywallet. 7 | 8 | import struct 9 | 10 | 11 | class SerializationError(Exception): 12 | pass 13 | 14 | 15 | class BCDataStream(object): 16 | def __init__(self, input): 17 | self.input = bytes(input) 18 | self.read_cursor = 0 19 | 20 | def read_string(self): 21 | # Strings are encoded depending on length: 22 | # 0 to 252 : 1-byte-length followed by bytes (if any) 23 | # 253 to 65,535 : byte'253' 2-byte-length followed by bytes 24 | # 65,536 to 4,294,967,295 : byte '254' 4-byte-length followed by bytes 25 | # ... and the Bitcoin client is coded to understand: 26 | # greater than 4,294,967,295 : byte '255' 8-byte-length followed by bytes of string 27 | # ... but I don't think it actually handles any strings that big. 28 | try: 29 | length = self.read_compact_size() 30 | except IndexError: 31 | raise SerializationError("attempt to read past end of buffer") 32 | 33 | return self.read_bytes(length) 34 | 35 | def read_bytes(self, length): 36 | try: 37 | result = self.input[self.read_cursor:self.read_cursor + length] 38 | self.read_cursor += length 39 | return result 40 | except IndexError: 41 | raise SerializationError("attempt to read past end of buffer") 42 | 43 | def read_boolean(self): 44 | return self.read_bytes(1)[0] != chr(0) 45 | 46 | def read_int16(self): 47 | return self._read_num('= b58_base: 18 | div, mod = divmod(long_value, b58_base) 19 | result = b58_chars[mod] + result 20 | long_value = div 21 | result = b58_chars[long_value] + result 22 | 23 | # Bitcoin does a little leading-zero-compression: 24 | # leading 0-bytes in the input become leading-1s 25 | nPad = 0 26 | for c in v: 27 | if c == 0: 28 | nPad += 1 29 | else: 30 | break 31 | 32 | return (b58_chars[0] * nPad) + result 33 | 34 | 35 | def b58decode(v, length): 36 | """ decode v into a string of len bytes 37 | """ 38 | long_value = 0 39 | for (i, c) in enumerate(v[::-1]): 40 | long_value += b58_chars.find(c) * (b58_base ** i) 41 | 42 | result = '' 43 | while long_value >= 256: 44 | div, mod = divmod(long_value, 256) 45 | result = chr(mod) + result 46 | long_value = div 47 | result = chr(long_value) + result 48 | 49 | nPad = 0 50 | for c in v: 51 | if c == b58_chars[0]: 52 | nPad += 1 53 | else: 54 | break 55 | 56 | result = chr(0) * nPad + result 57 | if length is not None and len(result) != length: 58 | return None 59 | 60 | return result 61 | 62 | 63 | def double_sha256(data): 64 | return hashlib.sha256(hashlib.sha256(data).digest()).digest() 65 | 66 | 67 | def encode_base58_check(secret): 68 | hash = double_sha256(secret) 69 | return b58encode(secret + hash[0:4]) 70 | 71 | 72 | def privkey_to_secret(privkey): 73 | if len(privkey) == 279: 74 | return privkey[9:9 + 32] 75 | else: 76 | return privkey[8:8 + 32] 77 | 78 | 79 | def secret_to_asecret(secret, version): 80 | prefix = (version + 128) & 255 81 | vchIn = bytes([prefix]) + secret 82 | return encode_base58_check(vchIn) 83 | 84 | 85 | def hash_160(public_key): 86 | md = hashlib.new('ripemd160') 87 | md.update(hashlib.sha256(public_key).digest()) 88 | return md.digest() 89 | 90 | 91 | def public_key_to_bc_address(public_key, version): 92 | h160 = hash_160(public_key) 93 | return hash_160_to_bc_address(h160, version) 94 | 95 | 96 | def hash_160_to_bc_address(h160, version): 97 | vh160 = bytes([int(version)]) + h160 98 | h = double_sha256(vh160) 99 | addr = vh160 + h[0:4] 100 | return b58encode(addr) 101 | 102 | 103 | def bc_address_to_hash_160(addr): 104 | bytes = b58decode(addr, 25) 105 | return bytes[1:21] 106 | -------------------------------------------------------------------------------- /walletool/wallet_items.py: -------------------------------------------------------------------------------- 1 | # -- encoding: UTF-8 -- 2 | import socket 3 | from binascii import hexlify 4 | 5 | from walletool.bc_data_stream import BCDataStream 6 | from walletool.utils import privkey_to_secret, secret_to_asecret, public_key_to_bc_address 7 | 8 | 9 | def parse_TxIn(vds): 10 | d = {} 11 | d['prevout_hash'] = vds.read_bytes(32) 12 | d['prevout_n'] = vds.read_uint32() 13 | d['scriptSig'] = vds.read_bytes(vds.read_compact_size()) 14 | d['sequence'] = vds.read_uint32() 15 | return d 16 | 17 | 18 | def parse_TxOut(vds): 19 | d = {} 20 | d['value'] = vds.read_int64() / 1e8 21 | d['scriptPubKey'] = vds.read_bytes(vds.read_compact_size()) 22 | return d 23 | 24 | 25 | def inversetxid(txid): 26 | txid = hexlify(txid).decode() 27 | if len(txid) != 64: 28 | raise ValueError('txid %r length != 64' % txid) 29 | new_txid = "" 30 | for i in range(32): 31 | new_txid += txid[62 - 2 * i] 32 | new_txid += txid[62 - 2 * i + 1] 33 | return new_txid 34 | 35 | 36 | def parse_CAddress(vds): 37 | d = {'ip': '0.0.0.0', 'port': 0, 'nTime': 0} 38 | try: 39 | d['nVersion'] = vds.read_int32() 40 | d['nTime'] = vds.read_uint32() 41 | d['nServices'] = vds.read_uint64() 42 | d['pchReserved'] = vds.read_bytes(12) 43 | d['ip'] = socket.inet_ntoa(vds.read_bytes(4)) 44 | d['port'] = vds.read_uint16() 45 | except: 46 | pass 47 | return d 48 | 49 | 50 | def parse_BlockLocator(vds): 51 | d = {'hashes': []} 52 | nHashes = vds.read_compact_size() 53 | for i in range(nHashes): 54 | d['hashes'].append(vds.read_bytes(32)) 55 | return d 56 | 57 | 58 | def parse_setting(setting, vds): 59 | if setting[0] == "f": # flag (boolean) settings 60 | return str(vds.read_boolean()) 61 | elif setting[0:4] == "addr": # CAddress 62 | return parse_CAddress(vds) 63 | elif setting == "nTransactionFee": 64 | return vds.read_int64() 65 | elif setting == "nLimitProcessors": 66 | return vds.read_int32() 67 | return {'unknown': vds} 68 | 69 | 70 | class WalletItem: 71 | item_type = None 72 | 73 | def __init__(self, key, value, type, data): 74 | self.key = key 75 | self.value = value 76 | self.type = type 77 | self.data = data 78 | 79 | def __repr__(self): 80 | return '<%s item: %s>' % (self.type, self.data) 81 | 82 | @classmethod 83 | def parse(cls, key, value): 84 | kds = BCDataStream(key) 85 | vds = BCDataStream(value) 86 | type = kds.read_string().decode() 87 | data = {} 88 | 89 | # From Pywallet: 90 | 91 | if type == 'tx': 92 | data['tx_id'] = inversetxid(kds.read_bytes(32)) 93 | start = vds.read_cursor 94 | data['version'] = vds.read_int32() 95 | n_vin = vds.read_compact_size() 96 | data['txIn'] = [] 97 | for i in range(n_vin): 98 | data['txIn'].append(parse_TxIn(vds)) 99 | n_vout = vds.read_compact_size() 100 | data['txOut'] = [] 101 | for i in range(n_vout): 102 | data['txOut'].append(parse_TxOut(vds)) 103 | data['lockTime'] = vds.read_uint32() 104 | data['tx'] = vds.input[start:vds.read_cursor] 105 | data['txv'] = value 106 | data['txk'] = key 107 | elif type == 'name': 108 | data['hash'] = kds.read_string() 109 | data['name'] = vds.read_string() 110 | elif type == 'version': 111 | data['version'] = vds.read_uint32() 112 | elif type == 'minversion': 113 | data['minversion'] = vds.read_uint32() 114 | elif type == 'setting': 115 | data['setting'] = kds.read_string() 116 | data['value'] = parse_setting(data['setting'].decode(), vds) 117 | elif type == 'key': 118 | data['public_key'] = kds.read_bytes(kds.read_compact_size()) 119 | data['private_key'] = vds.read_bytes(vds.read_compact_size()) 120 | elif type == 'wkey': 121 | data['public_key'] = kds.read_bytes(kds.read_compact_size()) 122 | data['private_key'] = vds.read_bytes(vds.read_compact_size()) 123 | data['created'] = vds.read_int64() 124 | data['expires'] = vds.read_int64() 125 | data['comment'] = vds.read_string() 126 | elif type == 'defaultkey': 127 | data['key'] = vds.read_bytes(vds.read_compact_size()) 128 | elif type == 'pool': 129 | data['n'] = kds.read_int64() 130 | data['nVersion'] = vds.read_int32() 131 | data['nTime'] = vds.read_int64() 132 | data['public_key'] = vds.read_bytes(vds.read_compact_size()) 133 | elif type == 'acc': 134 | data['account'] = kds.read_string() 135 | data['nVersion'] = vds.read_int32() 136 | data['public_key'] = vds.read_bytes(vds.read_compact_size()) 137 | elif type == 'acentry': 138 | data['account'] = kds.read_string() 139 | data['n'] = kds.read_uint64() 140 | data['nVersion'] = vds.read_int32() 141 | data['nCreditDebit'] = vds.read_int64() 142 | data['nTime'] = vds.read_int64() 143 | data['otherAccount'] = vds.read_string() 144 | data['comment'] = vds.read_string() 145 | elif type == 'bestblock': 146 | data['nVersion'] = vds.read_int32() 147 | data.update(parse_BlockLocator(vds)) 148 | elif type == 'ckey': 149 | data['public_key'] = kds.read_bytes(kds.read_compact_size()) 150 | data['encrypted_private_key'] = vds.read_bytes(vds.read_compact_size()) 151 | elif type == 'mkey': 152 | data['nID'] = kds.read_uint32() 153 | data['encrypted_key'] = vds.read_string() 154 | data['salt'] = vds.read_string() 155 | data['nDerivationMethod'] = vds.read_uint32() 156 | data['nDerivationIterations'] = vds.read_uint32() 157 | data['otherParams'] = vds.read_string() 158 | 159 | for item_cls in cls.__subclasses__(): 160 | if item_cls.item_type == type: 161 | break 162 | else: 163 | item_cls = cls 164 | 165 | return item_cls(key, value, type, data) 166 | 167 | 168 | class KeyWalletItem(WalletItem): 169 | item_type = 'key' 170 | 171 | def get_address(self, version): 172 | return public_key_to_bc_address(self.data['public_key'], version=version) 173 | 174 | def get_private_key(self, version): 175 | secret = privkey_to_secret(self.data['private_key']) 176 | asecret = secret_to_asecret(secret, version=version) 177 | return asecret 178 | 179 | 180 | def parse_wallet_dict(wallet_dict): 181 | for key, value in wallet_dict.items(): 182 | yield WalletItem.parse(key, value) 183 | --------------------------------------------------------------------------------