Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 31 additions & 14 deletions btcrecover/btcrpass.py
Original file line number Diff line number Diff line change
Expand Up @@ -4022,6 +4022,7 @@ def __init__(
disable_p2tr=False,
disable_bip44=False,
disable_bip84=False,
fingerprint=None,
):
from . import btcrseed

Expand Down Expand Up @@ -4075,6 +4076,7 @@ def __init__(
disable_p2tr=disable_p2tr,
disable_bip44=disable_bip44,
disable_bip84=disable_bip84,
fingerprint=fingerprint,
)

if is_performance and not mnemonic:
Expand Down Expand Up @@ -4174,7 +4176,8 @@ class WalletSLIP39(object):
opencl_algo = -1

def __init__(self, mpk = None, addresses = None, address_limit = None, addressdb_filename = None,
slip39_shares = None, lang = None, path = None, wallet_type = "bip39", is_performance = False):
slip39_shares = None, lang = None, path = None, wallet_type = "bip39", is_performance = False,
fingerprint = None):

if not shamir_mnemonic_available:
print()
Expand Down Expand Up @@ -4216,7 +4219,8 @@ def __init__(self, mpk = None, addresses = None, address_limit = None, addressdb
hash160s = None

self.btcrseed_wallet = btcrseed_cls.create_from_params(
mpk, addresses, address_limit, hash160s, path, is_performance)
mpk, addresses, address_limit, hash160s, path, is_performance,
fingerprint=fingerprint)

self.btcrseed_wallet._derivation_salts = [""]

Expand Down Expand Up @@ -4352,7 +4356,8 @@ class WalletCardano(WalletBIP39):
opencl_algo = -1

def __init__(self, addresses=None, addressdb_filename=None,
mnemonic=None, lang=None, path=None, is_performance=False):
mnemonic=None, lang=None, path=None, is_performance=False,
fingerprint=None):
from . import btcrseed

btcrseed_cls = btcrecover.btcrseed.WalletCardano
Expand All @@ -4373,7 +4378,8 @@ def __init__(self, addresses=None, addressdb_filename=None,
else:
hash160s = None

self.btcrseed_wallet = btcrseed_cls.create_from_params(addresses=addresses)
self.btcrseed_wallet = btcrseed_cls.create_from_params(
addresses=addresses, fingerprint=fingerprint)
#addresses, hash160s, path, is_performance)

if is_performance and not mnemonic:
Expand Down Expand Up @@ -4499,7 +4505,8 @@ def _return_verified_password_or_false_opencl(self, arg_passwords):
############### Py_Crypto_HD_Wallet Based Wallets ####################
class WalletPyCryptoHDWallet(WalletBIP39):
def __init__(self, mpk = None, addresses = None, address_limit = None, addressdb_filename = None,
mnemonic = None, lang = None, path = None, wallet_type = "bip39", is_performance = False):
mnemonic = None, lang = None, path = None, wallet_type = "bip39", is_performance = False,
fingerprint = None):
from . import btcrseed

wallet_type = wallet_type.lower()
Expand Down Expand Up @@ -4531,7 +4538,8 @@ def __init__(self, mpk = None, addresses = None, address_limit = None, addressdb
hash160s = None

self.btcrseed_wallet = btcrseed_cls.create_from_params(
mpk, addresses, address_limit, hash160s, path, is_performance)
mpk, addresses, address_limit, hash160s, path, is_performance,
fingerprint=fingerprint)

if is_performance and not mnemonic:
mnemonic = "certain come keen collect slab gauge photo inside mechanic deny leader drop"
Expand Down Expand Up @@ -4570,7 +4578,8 @@ def _return_verified_password_or_false_cpu(self, passwords):
class WalletEthereumValidator(WalletBIP39):

def __init__(self, mpk = None, addresses = None, address_limit = None, addressdb_filename = None,
mnemonic = None, lang = None, path = None, wallet_type = "EthereumValidator", is_performance = False):
mnemonic = None, lang = None, path = None, wallet_type = "EthereumValidator", is_performance = False,
fingerprint = None):
from . import btcrseed

btcrseed_cls = btcrseed.WalletEthereumValidator
Expand All @@ -4591,7 +4600,8 @@ def __init__(self, mpk = None, addresses = None, address_limit = None, addressdb
hash160s = None

self.btcrseed_wallet = btcrseed_cls.create_from_params(
mpk, addresses, address_limit, hash160s, path, is_performance)
mpk, addresses, address_limit, hash160s, path, is_performance,
fingerprint=fingerprint)

if is_performance and not mnemonic:
mnemonic = "certain come keen collect slab gauge photo inside mechanic deny leader drop"
Expand Down Expand Up @@ -6106,6 +6116,7 @@ def init_parser_common():
bip39_group.add_argument("--slip39", action="store_true", help="search for a SLIP-39 passphrase instead of from a wallet")
bip39_group.add_argument("--slip39-shares", metavar="SLIP39-MNEMONIC", nargs="+", help="SLIP39 Share Mnemonics")
bip39_group.add_argument("--mpk", metavar="XPUB", help="the master public key")
bip39_group.add_argument("--fingerprint", metavar="HEX", help="wallet master fingerprint (4-byte hex) instead of an xpub or addresses")
bip39_group.add_argument("--addr-limit", type=int, metavar="COUNT", help="if using addrs or addressdb, the generation limit")
bip39_group.add_argument("--language", metavar="LANG-CODE", help="the wordlist language to use (see wordlists/README.md, default: auto)")
bip39_group.add_argument("--bip32-path", metavar="PATH", nargs="+", help="path (e.g. m/0'/0/) excluding the final index. You can specify multiple derivation paths seperated by a space Eg: m/84'/0'/0'/0 m/84'/0'/1'/0 (default: BIP44,BIP49 & BIP84 account 0)")
Expand Down Expand Up @@ -6853,19 +6864,24 @@ def _apply_beep_configuration(parsed_args):

if args.wallet_type == "cardano":
loaded_wallet = WalletCardano(args.addrs, args.addressdb, mnemonic,
args.language, args.bip32_path, args.performance)
args.language, args.bip32_path, args.performance,
fingerprint=args.fingerprint)
elif args.wallet_type in ['avalanche', 'tron', 'solana', 'cosmos', 'tezos','stellar','multiversx']:
loaded_wallet = WalletPyCryptoHDWallet(args.mpk, args.addrs, args.addr_limit, args.addressdb, mnemonic,
args.language, args.bip32_path, args.wallet_type, args.performance)
args.language, args.bip32_path, args.wallet_type, args.performance,
fingerprint=args.fingerprint)
elif args.wallet_type in ['polkadotsubstrate']:
loaded_wallet = WalletPyCryptoHDWallet(args.mpk, args.addrs, args.addr_limit, args.addressdb, mnemonic,
args.language, args.substrate_path, args.wallet_type, args.performance)
args.language, args.substrate_path, args.wallet_type, args.performance,
fingerprint=args.fingerprint)
elif args.wallet_type == "ethereumvalidator":
loaded_wallet = WalletEthereumValidator(args.mpk, args.addrs, args.addr_limit, args.addressdb, mnemonic,
args.language, args.bip32_path, args.wallet_type, args.performance)
args.language, args.bip32_path, args.wallet_type, args.performance,
fingerprint=args.fingerprint)
elif args.slip39:
loaded_wallet = WalletSLIP39(args.mpk, args.addrs, args.addr_limit, args.addressdb, args.slip39_shares,
args.language, args.bip32_path, args.wallet_type, args.performance)
args.language, args.bip32_path, args.wallet_type, args.performance,
fingerprint=args.fingerprint)
else:
loaded_wallet = WalletBIP39(args.mpk, args.addrs, args.addr_limit, args.addressdb, mnemonic,
args.language, args.bip32_path, args.wallet_type, args.performance,
Expand All @@ -6877,7 +6893,8 @@ def _apply_beep_configuration(parsed_args):
disable_p2sh = args.disable_p2sh,
disable_p2tr = args.disable_p2tr,
disable_bip44 = args.disable_bip44,
disable_bip84 = args.disable_bip84)
disable_bip84 = args.disable_bip84,
fingerprint=args.fingerprint)


if args.yoroi_master_password:
Expand Down
97 changes: 68 additions & 29 deletions btcrecover/btcrseed.py
Original file line number Diff line number Diff line change
Expand Up @@ -1120,6 +1120,8 @@ def __init__(self, arg_derivationpath = None, loading = False):
super(WalletBIP32, self).__init__(loading)
self._chaincode = None
self._passwords_per_second = None
self._fingerprint = None
self._addrs_to_generate = 0

derivation_paths = []
self._append_last_index = False
Expand Down Expand Up @@ -1401,10 +1403,23 @@ def _is_script_type_enabled(self, script_type):
# or a hash160s container. If none of these were supplied, prompts the user for each.
# (the BIP32 key derivation path is by default BIP44's account 0)
@classmethod
def create_from_params(cls, mpk = None, addresses = None, address_limit = None, hash160s = None, path = None, is_performance = False, address_start_index = None, force_p2sh = False, checksinglexpubaddress = False, force_p2tr = False, force_bip44 = False, force_bip84 = False, disable_p2sh = False, disable_p2tr = False, disable_bip44 = False, disable_bip84 = False):
def create_from_params(cls, mpk = None, addresses = None, address_limit = None, hash160s = None, path = None, is_performance = False, address_start_index = None, force_p2sh = False, checksinglexpubaddress = False, force_p2tr = False, force_bip44 = False, force_bip84 = False, disable_p2sh = False, disable_p2tr = False, disable_bip44 = False, disable_bip84 = False, fingerprint = None):
self = cls(path, loading=True)

auto_detected_types = None
self._known_hash160s = None

if fingerprint:
normalized = fingerprint.strip()
if normalized.startswith("0x"):
normalized = normalized[2:]
try:
fingerprint_bytes = binascii.unhexlify(normalized)
except (binascii.Error, TypeError):
raise ValueError("the wallet fingerprint must be a hex value")
if len(fingerprint_bytes) != 4:
raise ValueError("the wallet fingerprint must be exactly 4 bytes (8 hex characters)")
self._fingerprint = fingerprint_bytes

# Process the mpk (master public key) argument
if mpk:
Expand Down Expand Up @@ -1469,7 +1484,7 @@ def create_from_params(cls, mpk = None, addresses = None, address_limit = None,
self._apply_script_type_filters()

# If mpk, addresses, and hash160s arguments were all not provided, prompt the user for an mpk first
if not mpk and not addresses and not hash160s:
if not mpk and not addresses and not hash160s and not self._fingerprint:
init_gui()
while True:
if tk_root: # Skip if TK is not available...
Expand Down Expand Up @@ -1540,7 +1555,7 @@ def create_from_params(cls, mpk = None, addresses = None, address_limit = None,

# If an mpk Testing Mnemonic:'t provided (at all), and addresses and hash160s arguments also
# weren't provided (in the original function call), prompt the user for addresses.
if not addresses and not hash160s:
if not addresses and not hash160s and not self._fingerprint:
# init_gui() was already called above
self._known_hash160s = None
while True:
Expand Down Expand Up @@ -1572,34 +1587,37 @@ def create_from_params(cls, mpk = None, addresses = None, address_limit = None,
if type(self) in [btcrecover.btcrseed.WalletPolkadotSubstrate]:
address_limit = 1

if not address_limit:
init_gui() # might not have been called yet
before_the = "one(s) you just entered" if addresses else "first one in actual use"

suggested_addr_limit = 10
# There are some wallets where account Generation limit isn't generally relevant, so suggest to set it as 1
if type(self) in [btcrecover.btcrseed.WalletTron, btcrecover.btcrseed.WalletEthereum, btcrecover.btcrseed.WalletSolana,
btcrecover.btcrseed.WalletCosmos, btcrecover.btcrseed.WalletStellar]:
suggested_addr_limit = 1

if tk_root: # Skip if TK is not available...
address_limit = tk.simpledialog.askinteger("Address limit",
"Please enter the address generation limit. Smaller will\n"
"be faster, but it must be equal to at least the number\n"
"of addresses created before the "+before_the+":\n"
"(If unsure, the number below is a sensible default...)", minvalue=1, initialvalue=suggested_addr_limit)
else:
print("No address generation limit specified... Exiting...")
exit()

if not self._fingerprint or self._known_hash160s:
if not address_limit:
sys.exit("canceled")
self._addrs_to_generate = address_limit
init_gui() # might not have been called yet
before_the = "one(s) you just entered" if addresses else "first one in actual use"

suggested_addr_limit = 10
# There are some wallets where account Generation limit isn't generally relevant, so suggest to set it as 1
if type(self) in [btcrecover.btcrseed.WalletTron, btcrecover.btcrseed.WalletEthereum, btcrecover.btcrseed.WalletSolana,
btcrecover.btcrseed.WalletCosmos, btcrecover.btcrseed.WalletStellar]:
suggested_addr_limit = 1

if tk_root: # Skip if TK is not available...
address_limit = tk.simpledialog.askinteger("Address limit",
"Please enter the address generation limit. Smaller will\n"
"be faster, but it must be equal to at least the number\n"
"of addresses created before the "+before_the+":\n"
"(If unsure, the number below is a sensible default...)", minvalue=1, initialvalue=suggested_addr_limit)
else:
print("No address generation limit specified... Exiting...")
exit()

if not self._known_hash160s:
print("Loading address database ...")
self._known_hash160s = AddressSet.fromfile(open(ADDRESSDB_DEF_FILENAME, "rb"))
print("Loaded", len(self._known_hash160s), "addresses from database ...")
if not address_limit:
sys.exit("canceled")
self._addrs_to_generate = address_limit

if not self._known_hash160s and not self._fingerprint:
print("Loading address database ...")
self._known_hash160s = AddressSet.fromfile(open(ADDRESSDB_DEF_FILENAME, "rb"))
print("Loaded", len(self._known_hash160s), "addresses from database ...")
else:
self._addrs_to_generate = address_limit if address_limit else 0

return self

Expand Down Expand Up @@ -1692,6 +1710,18 @@ def _verify_seed(self, arg_seed_bytes, salt = None):
if salt is None:
salt = self._derivation_salts[0]
# Derive the chain of private keys for the specified path as per BIP32

if self._fingerprint is not None:
privkey_bytes = arg_seed_bytes[:32]
try:
master_pubkey = coincurve.PublicKey.from_valid_secret(privkey_bytes).format()
except ValueError:
return False
master_fingerprint = ripemd160(hashlib.sha256(master_pubkey).digest())[:4]
if master_fingerprint != self._fingerprint:
return False
if not self._chaincode and not self._known_hash160s:
return True

if self.checksinglexpubaddress: #Atomic (Eth), MyBitcoinWallet, PT.BTC Wallet Single Address (Does things in a very non-standard way)
seed_bytes = arg_seed_bytes
Expand Down Expand Up @@ -2167,13 +2197,15 @@ def create_from_params(
disable_p2tr=False,
disable_bip44=False,
disable_bip84=False,
fingerprint=None,
):
if mpk or addresses or hash160s:
wallet = super(WalletAezeed, cls).create_from_params(
mpk=mpk,
addresses=addresses,
address_limit=address_limit,
hash160s=hash160s,
fingerprint=fingerprint,
path=path,
is_performance=is_performance,
address_start_index=address_start_index,
Expand Down Expand Up @@ -4585,6 +4617,7 @@ def main(argv):
parser.add_argument("--wallet", metavar="FILE", help="the wallet file")
parser.add_argument("--wallet-type", metavar="TYPE", help="if not using a wallet file, the wallet type")
parser.add_argument("--mpk", metavar="XPUB-OR-HEX", help="if not using a wallet file, the master public key (xpub, ypub or zpub)")
parser.add_argument("--fingerprint", metavar="HEX", help="wallet master fingerprint (4-byte hex) instead of an xpub or addresses")
parser.add_argument("--addrs", metavar="ADDRESS", nargs="+", help="if not using an mpk, address(es) in the wallet")
parser.add_argument("--addressdb", metavar="FILE", nargs="?", help="if not using addrs, use a full address database (default: %(const)s)", const=ADDRESSDB_DEF_FILENAME)
parser.add_argument("--addr-limit", type=int, metavar="COUNT", help="if using addrs or addressdb, the address generation limit")
Expand Down Expand Up @@ -4768,6 +4801,12 @@ def main(argv):
else:
create_from_params["mpk"] = args.mpk

if args.fingerprint:
if args.wallet:
print("warning: --fingerprint is ignored when a wallet is provided", file=sys.stderr)
else:
create_from_params["fingerprint"] = args.fingerprint

if args.addrs:
if args.wallet:
print("warning: --addrs is ignored when a wallet is provided", file=sys.stderr)
Expand Down
7 changes: 7 additions & 0 deletions btcrecover/test/test_passwords.py
Original file line number Diff line number Diff line change
Expand Up @@ -1925,6 +1925,13 @@ def test_skip_mnemonic_checksum(self):
)
btcrpass.args = argparse.Namespace()

@skipUnless(can_load_coincurve, "requires coincurve")
def test_bip39_fingerprint_only(self):
self.bip39_tester(
fingerprint="0xF9D16AAE",
mnemonic="certain come keen collect slab gauge photo inside mechanic deny leader drop",
)

def cardano_tester_opencl(self, *args, **kwargs):

wallet = btcrpass.WalletCardano(*args, **kwargs)
Expand Down
Loading
Loading