|
|
@ -26,12 +26,14 @@ |
|
|
|
|
|
|
|
from unicodedata import normalize |
|
|
|
import hashlib |
|
|
|
from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List |
|
|
|
import re |
|
|
|
from typing import Tuple, TYPE_CHECKING, Union, Sequence, Optional, Dict, List, NamedTuple |
|
|
|
|
|
|
|
from . import bitcoin, ecc, constants, bip32 |
|
|
|
from .bitcoin import deserialize_privkey, serialize_privkey |
|
|
|
from .bip32 import (convert_bip32_path_to_list_of_uint32, BIP32_PRIME, |
|
|
|
is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation) |
|
|
|
is_xpub, is_xprv, BIP32Node, normalize_bip32_derivation, |
|
|
|
convert_bip32_intpath_to_strpath) |
|
|
|
from .ecc import string_to_number, number_to_string |
|
|
|
from .crypto import (pw_decode, pw_encode, sha256, sha256d, PW_HASH_VERSION_LATEST, |
|
|
|
SUPPORTED_PW_HASH_VERSIONS, UnsupportedPasswordHashVersion, hash_160) |
|
|
@ -49,6 +51,7 @@ class KeyStore(Logger): |
|
|
|
|
|
|
|
def __init__(self): |
|
|
|
Logger.__init__(self) |
|
|
|
self.is_requesting_to_be_rewritten_to_wallet_file = False # type: bool |
|
|
|
|
|
|
|
def has_seed(self): |
|
|
|
return False |
|
|
@ -325,30 +328,60 @@ class Xpub: |
|
|
|
self.xpub_receive = None |
|
|
|
self.xpub_change = None |
|
|
|
|
|
|
|
# if these are None now, then it is the responsibility of the caller to |
|
|
|
# also call self.add_derivation_prefix_and_root_fingerprint: |
|
|
|
self._derivation_prefix = derivation_prefix # note: subclass should persist this |
|
|
|
self._root_fingerprint = root_fingerprint # note: subclass should persist this |
|
|
|
# "key origin" info (subclass should persist these): |
|
|
|
self._derivation_prefix = derivation_prefix # type: Optional[str] |
|
|
|
self._root_fingerprint = root_fingerprint # type: Optional[str] |
|
|
|
|
|
|
|
def get_master_public_key(self): |
|
|
|
return self.xpub |
|
|
|
|
|
|
|
def get_derivation_prefix(self) -> str: |
|
|
|
"""Returns to bip32 path from some root node to self.xpub""" |
|
|
|
assert self._derivation_prefix is not None, 'derivation_prefix should have been set already' |
|
|
|
def get_derivation_prefix(self) -> Optional[str]: |
|
|
|
"""Returns to bip32 path from some root node to self.xpub |
|
|
|
Note that the return value might be None; if it is unknown. |
|
|
|
""" |
|
|
|
return self._derivation_prefix |
|
|
|
|
|
|
|
def get_root_fingerprint(self) -> str: |
|
|
|
def get_root_fingerprint(self) -> Optional[str]: |
|
|
|
"""Returns the bip32 fingerprint of the top level node. |
|
|
|
This top level node is the node at the beginning of the derivation prefix, |
|
|
|
i.e. applying the derivation prefix to it will result self.xpub |
|
|
|
Note that the return value might be None; if it is unknown. |
|
|
|
""" |
|
|
|
assert self._root_fingerprint is not None, 'root_fingerprint should have been set already' |
|
|
|
return self._root_fingerprint |
|
|
|
|
|
|
|
def add_derivation_prefix_and_root_fingerprint(self, *, derivation_prefix: str, root_node: BIP32Node): |
|
|
|
def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int]) -> Tuple[bytes, Sequence[int]]: |
|
|
|
"""Returns fingerprint and 'full' derivation path corresponding to a derivation suffix. |
|
|
|
The fingerprint is either the root fp or the intermediate fp, depending on what is available, |
|
|
|
and the 'full' derivation path is adjusted accordingly. |
|
|
|
""" |
|
|
|
fingerprint_hex = self.get_root_fingerprint() |
|
|
|
der_prefix_str = self.get_derivation_prefix() |
|
|
|
if fingerprint_hex is not None and der_prefix_str is not None: |
|
|
|
# use root fp, and true full path |
|
|
|
fingerprint_bytes = bfh(fingerprint_hex) |
|
|
|
der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str) |
|
|
|
else: |
|
|
|
# use intermediate fp, and claim der suffix is the full path |
|
|
|
fingerprint_bytes = BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node() |
|
|
|
der_prefix_ints = convert_bip32_path_to_list_of_uint32('m') |
|
|
|
der_full = der_prefix_ints + list(der_suffix) |
|
|
|
return fingerprint_bytes, der_full |
|
|
|
|
|
|
|
def get_xpub_to_be_used_in_partial_tx(self) -> str: |
|
|
|
assert self.xpub |
|
|
|
fp_bytes, der_full = self.get_fp_and_derivation_to_be_used_in_partial_tx(der_suffix=[]) |
|
|
|
bip32node = BIP32Node.from_xkey(self.xpub) |
|
|
|
depth = len(der_full) |
|
|
|
child_number_int = der_full[-1] if len(der_full) >= 1 else 0 |
|
|
|
child_number_bytes = child_number_int.to_bytes(length=4, byteorder="big") |
|
|
|
fingerprint = bytes(4) if depth == 0 else bip32node.fingerprint |
|
|
|
bip32node = bip32node._replace(depth=depth, |
|
|
|
fingerprint=fingerprint, |
|
|
|
child_number=child_number_bytes) |
|
|
|
return bip32node.to_xpub() |
|
|
|
|
|
|
|
def add_key_origin_from_root_node(self, *, derivation_prefix: str, root_node: BIP32Node): |
|
|
|
assert self.xpub |
|
|
|
derivation_prefix = normalize_bip32_derivation(derivation_prefix) |
|
|
|
# try to derive ourselves from what we were given |
|
|
|
child_node1 = root_node.subkey_at_private_derivation(derivation_prefix) |
|
|
|
child_pubkey_bytes1 = child_node1.eckey.get_public_key_bytes(compressed=True) |
|
|
@ -356,14 +389,13 @@ class Xpub: |
|
|
|
child_pubkey_bytes2 = child_node2.eckey.get_public_key_bytes(compressed=True) |
|
|
|
if child_pubkey_bytes1 != child_pubkey_bytes2: |
|
|
|
raise Exception("(xpub, derivation_prefix, root_node) inconsistency") |
|
|
|
# store |
|
|
|
self._root_fingerprint = root_node.calc_fingerprint_of_this_node().hex().lower() |
|
|
|
self._derivation_prefix = derivation_prefix |
|
|
|
self.add_key_origin(derivation_prefix=derivation_prefix, |
|
|
|
root_fingerprint=root_node.calc_fingerprint_of_this_node().hex().lower()) |
|
|
|
|
|
|
|
def reset_derivation_prefix(self): |
|
|
|
def add_key_origin(self, *, derivation_prefix: Optional[str], root_fingerprint: Optional[str]): |
|
|
|
assert self.xpub |
|
|
|
self._derivation_prefix = 'm' |
|
|
|
self._root_fingerprint = BIP32Node.from_xkey(self.xpub).calc_fingerprint_of_this_node().hex().lower() |
|
|
|
self._root_fingerprint = root_fingerprint |
|
|
|
self._derivation_prefix = normalize_bip32_derivation(derivation_prefix) |
|
|
|
|
|
|
|
def derive_pubkey(self, for_change, n) -> str: |
|
|
|
for_change = int(for_change) |
|
|
@ -431,20 +463,22 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub): |
|
|
|
def is_watching_only(self): |
|
|
|
return self.xprv is None |
|
|
|
|
|
|
|
def add_xpub(self, xpub, *, default_der_prefix=True): |
|
|
|
def add_xpub(self, xpub): |
|
|
|
assert is_xpub(xpub) |
|
|
|
self.xpub = xpub |
|
|
|
if default_der_prefix: |
|
|
|
self.reset_derivation_prefix() |
|
|
|
root_fingerprint, derivation_prefix = bip32.root_fp_and_der_prefix_from_xkey(xpub) |
|
|
|
self.add_key_origin(derivation_prefix=derivation_prefix, root_fingerprint=root_fingerprint) |
|
|
|
|
|
|
|
def add_xprv(self, xprv, *, default_der_prefix=True): |
|
|
|
def add_xprv(self, xprv): |
|
|
|
assert is_xprv(xprv) |
|
|
|
self.xprv = xprv |
|
|
|
self.add_xpub(bip32.xpub_from_xprv(xprv), default_der_prefix=default_der_prefix) |
|
|
|
self.add_xpub(bip32.xpub_from_xprv(xprv)) |
|
|
|
|
|
|
|
def add_xprv_from_seed(self, bip32_seed, xtype, derivation): |
|
|
|
rootnode = BIP32Node.from_rootseed(bip32_seed, xtype=xtype) |
|
|
|
node = rootnode.subkey_at_private_derivation(derivation) |
|
|
|
self.add_xprv(node.to_xprv(), default_der_prefix=False) |
|
|
|
self.add_derivation_prefix_and_root_fingerprint(derivation_prefix=derivation, root_node=rootnode) |
|
|
|
self.add_xprv(node.to_xprv()) |
|
|
|
self.add_key_origin_from_root_node(derivation_prefix=derivation, root_node=rootnode) |
|
|
|
|
|
|
|
def get_private_key(self, sequence, password): |
|
|
|
xprv = self.get_master_private_key(password) |
|
|
@ -568,6 +602,15 @@ class Old_KeyStore(Deterministic_KeyStore): |
|
|
|
self._root_fingerprint = xfp.hex().lower() |
|
|
|
return self._root_fingerprint |
|
|
|
|
|
|
|
# TODO Old_KeyStore and Xpub could share a common baseclass? |
|
|
|
def get_fp_and_derivation_to_be_used_in_partial_tx(self, der_suffix: Sequence[int]) -> Tuple[bytes, Sequence[int]]: |
|
|
|
fingerprint_hex = self.get_root_fingerprint() |
|
|
|
der_prefix_str = self.get_derivation_prefix() |
|
|
|
fingerprint_bytes = bfh(fingerprint_hex) |
|
|
|
der_prefix_ints = convert_bip32_path_to_list_of_uint32(der_prefix_str) |
|
|
|
der_full = der_prefix_ints + list(der_suffix) |
|
|
|
return fingerprint_bytes, der_full |
|
|
|
|
|
|
|
def update_password(self, old_password, new_password): |
|
|
|
self.check_password(old_password) |
|
|
|
if new_password == '': |
|
|
@ -658,6 +701,14 @@ class Hardware_KeyStore(KeyStore, Xpub): |
|
|
|
def ready_to_sign(self): |
|
|
|
return super().ready_to_sign() and self.has_usable_connection_with_device() |
|
|
|
|
|
|
|
def opportunistically_fill_in_missing_info_from_device(self, client): |
|
|
|
assert client is not None |
|
|
|
if self._root_fingerprint is None: |
|
|
|
root_xpub = client.get_xpub('m', xtype='standard') |
|
|
|
root_fingerprint = BIP32Node.from_xkey(root_xpub).calc_fingerprint_of_this_node().hex().lower() |
|
|
|
self._root_fingerprint = root_fingerprint |
|
|
|
self.is_requesting_to_be_rewritten_to_wallet_file = True |
|
|
|
|
|
|
|
|
|
|
|
def bip39_normalize_passphrase(passphrase): |
|
|
|
return normalize('NFKD', passphrase or '') |
|
|
@ -718,16 +769,17 @@ PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES) |
|
|
|
|
|
|
|
def xtype_from_derivation(derivation: str) -> str: |
|
|
|
"""Returns the script type to be used for this derivation.""" |
|
|
|
if derivation.startswith("m/84'"): |
|
|
|
return 'p2wpkh' |
|
|
|
elif derivation.startswith("m/49'"): |
|
|
|
return 'p2wpkh-p2sh' |
|
|
|
elif derivation.startswith("m/44'"): |
|
|
|
return 'standard' |
|
|
|
elif derivation.startswith("m/45'"): |
|
|
|
return 'standard' |
|
|
|
|
|
|
|
bip32_indices = convert_bip32_path_to_list_of_uint32(derivation) |
|
|
|
if len(bip32_indices) >= 1: |
|
|
|
if bip32_indices[0] == 84 + BIP32_PRIME: |
|
|
|
return 'p2wpkh' |
|
|
|
elif bip32_indices[0] == 49 + BIP32_PRIME: |
|
|
|
return 'p2wpkh-p2sh' |
|
|
|
elif bip32_indices[0] == 44 + BIP32_PRIME: |
|
|
|
return 'standard' |
|
|
|
elif bip32_indices[0] == 45 + BIP32_PRIME: |
|
|
|
return 'standard' |
|
|
|
|
|
|
|
if len(bip32_indices) >= 4: |
|
|
|
if bip32_indices[0] == 48 + BIP32_PRIME: |
|
|
|
# m / purpose' / coin_type' / account' / script_type' / change / address_index |
|
|
@ -770,7 +822,7 @@ def load_keystore(storage, name) -> KeyStore: |
|
|
|
|
|
|
|
def is_old_mpk(mpk: str) -> bool: |
|
|
|
try: |
|
|
|
int(mpk, 16) |
|
|
|
int(mpk, 16) # test if hex string |
|
|
|
except: |
|
|
|
return False |
|
|
|
if len(mpk) != 128: |
|
|
@ -804,16 +856,18 @@ def is_private_key_list(text, *, allow_spaces_inside_key=True, raise_on_error=Fa |
|
|
|
raise_on_error=raise_on_error)) |
|
|
|
|
|
|
|
|
|
|
|
is_mpk = lambda x: is_old_mpk(x) or is_xpub(x) |
|
|
|
is_private = lambda x: is_seed(x) or is_xprv(x) or is_private_key_list(x) |
|
|
|
is_master_key = lambda x: is_old_mpk(x) or is_xprv(x) or is_xpub(x) |
|
|
|
is_private_key = lambda x: is_xprv(x) or is_private_key_list(x) |
|
|
|
is_bip32_key = lambda x: is_xprv(x) or is_xpub(x) |
|
|
|
def is_master_key(x): |
|
|
|
return is_old_mpk(x) or is_bip32_key(x) |
|
|
|
|
|
|
|
|
|
|
|
def is_bip32_key(x): |
|
|
|
return is_xprv(x) or is_xpub(x) |
|
|
|
|
|
|
|
|
|
|
|
def bip44_derivation(account_id, bip43_purpose=44): |
|
|
|
coin = constants.net.BIP44_COIN_TYPE |
|
|
|
return "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id)) |
|
|
|
der = "m/%d'/%d'/%d'" % (bip43_purpose, coin, int(account_id)) |
|
|
|
return normalize_bip32_derivation(der) |
|
|
|
|
|
|
|
|
|
|
|
def purpose48_derivation(account_id: int, xtype: str) -> str: |
|
|
@ -824,7 +878,8 @@ def purpose48_derivation(account_id: int, xtype: str) -> str: |
|
|
|
script_type_int = PURPOSE48_SCRIPT_TYPES.get(xtype) |
|
|
|
if script_type_int is None: |
|
|
|
raise Exception('unknown xtype: {}'.format(xtype)) |
|
|
|
return "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int) |
|
|
|
der = "m/%d'/%d'/%d'/%d'" % (bip43_purpose, coin, account_id, script_type_int) |
|
|
|
return normalize_bip32_derivation(der) |
|
|
|
|
|
|
|
|
|
|
|
def from_seed(seed, passphrase, is_p2sh=False): |
|
|
|