add bitcoin address verification

This commit is contained in:
Nick Klockenga
2021-10-11 21:06:22 -04:00
parent d6a00c81ee
commit a82a680630
4 changed files with 363 additions and 13 deletions
+201 -2
View File
@@ -1,10 +1,12 @@
# External Dependencies
import time
import re
from multiprocessing import Process, Queue
from subprocess import call
import os, sys
from embit import bip32
from embit import bip32, script, ec
from embit.networks import NETWORKS
from embit.descriptor import Descriptor
from binascii import hexlify
from threading import Thread
@@ -516,7 +518,6 @@ class Controller(Singleton):
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
xprv = root.derive(derivation)
xpub = xprv.to_public()
print(derivation)
xpub_base58 = xpub.to_string(version=version)
self.signing_tools_view.display_xpub_info(fingerprint, derivation, xpub_base58)
@@ -654,6 +655,204 @@ class Controller(Singleton):
else:
return Path.MAIN_MENU
elif decoder.isComplete() and decoder.isAddress():
address = decoder.getAddress()
address_type = decoder.getAddressType()
# address_parts = textwrap.wrap(address, 30)
address_parts = [address[i:i+22] for i in range(0, len(address), 22)]
address_part_1 = address_parts[0]
address_part_2 = address_parts[1] if len(address_parts) >= 2 else ""
address_part_3 = address_parts[2] if len(address_parts) >= 3 else ""
self.menu_view.draw_modal(["Bitcoin Address", address_part_1, address_part_2, address_part_3], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
validate_network = NETWORKS[self.settings.network]
if "main" in address_type:
validate_network = NETWORKS["main"]
elif "test" in address_type:
validate_network = NETWORKS["test"]
r = 0
if address_type in ("Bech32-main", "Bech32-test") and len(address) == 62:
r = 2
else:
# check single sig vs multi sig address
r = self.menu_view.display_generic_selection_menu(["Single Sig Seed", "JSON Wallet Desc"], "Validate Bitcoin Address")
if r == 1:
# validate single sig using seed
# No valid seed yet, If there is a saved seed, ask to use saved seed
if self.storage.num_of_saved_seeds() > 0:
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Use Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,3,None)
if slot_num == 0:
return Path.MAIN_MENU
seed = self.storage.get_seed(slot_num)
used_saved_seed = True
if not seed:
# no valid seed yet, gather seed phrase
# display menu to select 12 or 24 word seed for last word
ret_val = self.menu_view.display_qr_12_24_word_menu("... [ Return to Main Menu ]")
if ret_val == Path.SEED_WORD_12:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(12)
elif ret_val == Path.SEED_WORD_24:
seed.mnemonic = self.seed_tools_view.display_manual_seed_entry(24)
elif ret_val == Path.SEED_WORD_QR:
seed.mnemonic = self.seed_tools_view.read_seed_phrase_qr()
else:
return Path.MAIN_MENU
if not seed:
return Path.MAIN_MENU
# check if seed phrase is valid
self.menu_view.draw_modal(["Validating Seed ..."])
if not seed:
self.menu_view.draw_modal(["Seed Invalid", "check seed phrase", "and try again"], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
if len(seed.passphrase) == 0:
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Add Seed Passphrase?")
if r == 1:
# display a tool to pick letters/numbers to make a passphrase
seed.passphrase = self.seed_tools_view.draw_passphrase_keyboard_entry()
if len(seed.passphrase) == 0:
self.menu_view.draw_modal(["No passphrase added", "to seed words"], "", "Left to Exit, Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT, B.KEY_LEFT])
if input == B.KEY_LEFT:
return Path.MAIN_MENU
else:
self.menu_view.draw_modal(["Optional passphrase", "added to seed words", seed.passphrase], "", "Right to Continue")
self.buttons.wait_for([B.KEY_RIGHT])
# display seed phrase
while True:
r = self.seed_tools_view.display_seed_phrase(seed.mnemonic_list, seed.passphrase, "Right to Continue")
if r == True:
break
else:
# Cancel
return Path.MAIN_MENU
# Ask to save seed
if self.storage.slot_avaliable() and used_saved_seed == False:
r = self.menu_view.display_generic_selection_menu(["Yes", "No"], "Save Seed?")
if r == 1: #Yes
slot_num = self.menu_view.display_saved_seed_menu(self.storage,2,None)
if slot_num in (1,2,3):
self.storage.add_seed(seed, slot_num)
self.menu_view.draw_modal(["Seed Valid", "Saved to Slot #" + str(slot_num)], "", "Right to Continue")
input = self.buttons.wait_for([B.KEY_RIGHT])
#
# Validate if address from seed
#
# choose derivation standard
r = self.menu_view.display_generic_selection_menu(["Native Segwit", "Nested Segwit", "Custom"], "Derivation Path?")
if r == 1:
script_type = "native segwit"
elif r == 2:
script_type = "nested segwit"
elif r == 3:
script_type = "custom"
# calculated derivation or get custom from keyboard entry
if script_type == "custom":
derivation = self.settings_tools_view.draw_derivation_keyboard_entry(existing_derivation=self.settings.custom_derivation)
self.settings.custom_derivation = derivation # save for next time
else:
derivation = Settings.calc_derivation(validate_network, "single sig", script_type)
if derivation == "" or derivation == None:
self.menu_view.draw_modal(["Invalid Derivation", "try again"], "", "Right to Continue")
return Path.SEED_TOOLS_SUB_MENU
self.menu_view.draw_modal(["Checking Address", ""], "", "")
version = bip32.detect_version(derivation, default="xpub", network=validate_network)
root = bip32.HDKey.from_seed(seed.seed, version=validate_network["xprv"])
fingerprint = hexlify(root.child(0).fingerprint).decode('utf-8')
xprv = root.derive(derivation)
xpub = xprv.to_public()
for i in range(500):
r_pubkey = xpub.derive([0,i]).key
c_pubkey = xpub.derive([1,i]).key
recieve_address = ""
change_address = ""
if "P2PKH" in address_type:
recieve_address = script.p2pkh(r_pubkey).address(network=validate_network)
change_address = script.p2pkh(c_pubkey).address(network=validate_network)
elif "Bech32" in address_type:
recieve_address = script.p2wpkh(r_pubkey).address(network=validate_network)
change_address = script.p2wpkh(c_pubkey).address(network=validate_network)
elif "P2SH" in address_type:
recieve_address = script.p2sh(script.p2wpkh(r_pubkey)).address(network=validate_network)
change_address = script.p2sh(script.p2wpkh(c_pubkey)).address(network=validate_network)
if address == recieve_address:
self.menu_view.draw_modal(["Recieved Address "+str(i), "Verified"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
if address == change_address:
self.menu_view.draw_modal(["Change Address "+str(i), "Verified"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
else:
self.menu_view.draw_modal(["Checking Address "+str(i), "..."], "", "Right to Abort")
if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT):
return Path.MAIN_MENU
if r == 2:
# second QR scan need PSBT now
decoder2 = scan_qr("Scan Backup JSON QR")
if decoder2.isComplete() and decoder2.isWalletDescriptor():
desc_str = decoder2.getWalletDescriptor()
try:
if len(re.findall(r'\[([0-9,a-f,A-F]+?)(\/[0-9,\/,h\']+?)\].*?(\/0\/\*)', desc_str)) > 0:
p = re.compile(r'(\[[0-9,a-f,A-F]+?\/[0-9,\/,h\']+?\].*?)(\/0\/\*)')
desc_str = p.sub(r'\1/{0,1}/*', desc_str)
except:
desc_str = decoder2.getWalletDescriptor()
desc = Descriptor.from_string(desc_str)
else:
return Path.MAIN_MENU
#
# Validate if address from descriptor
#
for i in range(1000):
r_derived = desc.derive(i,branch_index=0)
c_derived = desc.derive(i,branch_index=1)
recieve_address = r_derived.address(validate_network)
change_address = c_derived.address(validate_network)
if address == recieve_address:
self.menu_view.draw_modal(["Recieved Address "+str(i), "Verified"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
if address == change_address:
self.menu_view.draw_modal(["Change Address "+str(i), "Verified"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
return Path.MAIN_MENU
else:
self.menu_view.draw_modal(["Checking Address "+str(i), "..."], "", "Right to Abort")
if self.buttons.check_for_low(B.KEY_RIGHT) or self.buttons.check_for_low(B.KEY_LEFT):
return Path.MAIN_MENU
elif ( decoder.isComplete() and not decoder.isPSBT() ) or decoder.isInvalid():
self.menu_view.draw_modal(["Not a valid PSBT QR"], "", "Right to Exit")
input = self.buttons.wait_for([B.KEY_RIGHT])
+123 -9
View File
@@ -2,6 +2,7 @@ from pyzbar import pyzbar
from pyzbar.pyzbar import ZBarSymbol
from enum import IntEnum
import re
import json
import base64
from embit import psbt
from binascii import a2b_base64, b2a_base64
@@ -25,6 +26,7 @@ class DecodeQR:
self.base64_qr = Base64DecodeQR() # Single Segments Base64
self.base43_qr = Base43DecodeQR() # Single Segment Base43
self.address_qr = BitcoinAddressQR() # Single Segment bitcoin address
self.specter_wallet_qr = SpecterDecodeWalletQR() # Specter Desktop Wallet Export decoder
self.wordlist = None
for key, value in kwargs.items():
@@ -115,6 +117,12 @@ class DecodeQR:
if rt == DecodeQRStatus.COMPLETE:
self.complete = True
return rt
elif self.qr_type == QRType.SPECTERWALLETQR:
rt = self.specter_wallet_qr.add(qr_str)
if rt == DecodeQRStatus.COMPLETE:
self.complete = True
return rt
else:
return DecodeQRStatus.INVALID
@@ -163,6 +171,9 @@ class DecodeQR:
def getAddressType(self):
return self.address_qr.getAddressType()
def getWalletDescriptor(self):
return self.specter_wallet_qr.getWalletDescriptor()
def getPercentComplete(self) -> int:
if self.qr_type == QRType.PSBTUR2:
@@ -207,7 +218,12 @@ class DecodeQR:
return False
def isAddress(self):
if self.qr_type in (QRType.BITCOINADDRESSQR):
if self.qr_type == QRType.BITCOINADDRESSQR:
return True
return False
def isWalletDescriptor(self):
if self.qr_type in (QRType.SPECTERWALLETQR, QRType.URWALLETQR, QRType.BLUEWALLETQR):
return True
return False
@@ -231,22 +247,36 @@ class DecodeQR:
# PSBT
if re.search("^UR:CRYPTO-PSBT/", s, re.IGNORECASE):
return QRType.PSBTUR2
elif re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE):
elif re.search(r'^p(\d+)of(\d+) ([A-Za-z0-9+\/=]+$)', s, re.IGNORECASE): #must be base64 characters only in segment
return QRType.PSBTSPECTER
elif re.search("^UR:BYTES/", s, re.IGNORECASE):
return QRType.PSBTURLEGACY
elif DecodeQR.isBase64PSBT(s):
return QRType.PSBTBASE64
# Bitcoin Address
if DecodeQR.isBitcoinAddress(s):
return QRType.BITCOINADDRESSQR
# Wallet Descriptor
elif re.search(r'^p(\d+)of(\d+) ', s, re.IGNORECASE):
# when not a SPECTER Base64 PSBT from above, assume it's json
return QRType.SPECTERWALLETQR
elif re.search(r'^\{\"label\".*\"descriptor\"\:.*', s, re.IGNORECASE):
# if json starting with label and contains descriptor, assume specter wallet json
return QRType.SPECTERWALLETQR
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
# create 4 letter wordlist only if not PSBT (performance gain)
try:
_4LETTER_WORDLIST = [word[:4].strip() for word in wordlist]
except:
_4LETTER_WORDLIST = []
# Seed
if re.search(r'\d{48,96}', s):
return QRType.SEEDSSQR
# Bitcoin Address
elif DecodeQR.isBitcoinAddress(s):
return QRType.BITCOINADDRESSQR
# Seed
elif all(x in wordlist for x in s.strip().split(" ")):
# checks if all words in list are in bip39 word list
return QRType.SEEDMNEMONIC
@@ -255,8 +285,8 @@ class DecodeQR:
return QRType.SEED4LETTERMNEMONIC
elif DecodeQR.isBase43PSBT(s):
return QRType.PSBTBASE43
else:
return QRType.INVALID
return QRType.INVALID
@staticmethod
def isBase64(s):
@@ -709,6 +739,90 @@ class BitcoinAddressQR:
return "Unknown"
return None
###
### SpecterDecodeWalletQR Class
### Purpose: used in DecodeQR to decode animated frames to get a wallet descriptor from Specter Desktop
###
class SpecterDecodeWalletQR:
def __init__(self):
self.total_segments = None
self.collected_segments = 0
self.complete = False
self.segments = []
def add(self, segment):
if self.total_segments == None:
self.total_segments = SpecterDecodeWalletQR.totalSegmentNum(segment)
self.segments = [None] * self.total_segments
elif self.total_segments != SpecterDecodeWalletQR.totalSegmentNum(segment):
raise Exception('Specter Desktop segment total changed unexpectedly')
if self.segments[SpecterDecodeWalletQR.currentSegmentNum(segment) - 1] == None:
self.segments[SpecterDecodeWalletQR.currentSegmentNum(segment) - 1] = SpecterDecodeWalletQR.parseSegment(segment)
self.collected_segments += 1
if self.total_segments == self.collected_segments:
if self.validateWalletDescriptor():
self.complete = True
return DecodeQRStatus.COMPLETE
else:
return DecodeQRStatus.INVALID
return DecodeQRStatus.PART_COMPLETE # new segment added
return DecodeQRStatus.PART_EXISTING # segment not added because it's already been added
def validateJson(self) -> str:
try:
j = "".join(self.segments)
json.loads(j)
except json.decoder.JSONDecodeError:
return False
return True
def validateWalletDescriptor(self):
if self.validateJson():
j = "".join(self.segments)
data = json.loads(j)
if "descriptor" in data:
return True
return False
def getWalletDescriptor(self) -> str:
if self.validateWalletDescriptor():
j = "".join(self.segments)
data = json.loads(j)
return data['descriptor']
return None
def is_complete(self) -> bool:
if self.complete and self.validateWalletDescriptor():
return True
return False
@staticmethod
def currentSegmentNum(segment) -> int:
if DecodeQR.SegmentType(segment) == QRType.SPECTERWALLETQR:
if re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE) != None:
return int(re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE).group(1))
else:
return 1
@staticmethod
def totalSegmentNum(segment) -> int:
if DecodeQR.SegmentType(segment) == QRType.SPECTERWALLETQR:
if re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE) != None:
return int(re.search(r'^p(\d+)of(\d+) ', segment, re.IGNORECASE).group(2))
else:
return 1
@staticmethod
def parseSegment(segment) -> str:
try:
return re.search(r'^p(\d+)of(\d+) (.+$)', segment, re.IGNORECASE).group(3)
except:
return segment
###
### DecodeQRStatus Class IntEum
### Purpose: used in DecodeQR to communicate status of adding qr frame/segment
+3
View File
@@ -18,4 +18,7 @@ class QRType(IntEnum):
XPUBQR = 20
SPECTERXPUBQR = 21
BITCOINADDRESSQR = 30
SPECTERWALLETQR = 40
URWALLETQR = 41
BLUEWALLETQR = 42
INVALID = 100
+36 -2
View File
@@ -404,6 +404,40 @@ def test_bitcoin_address():
assert d.getAddress() == "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq"
assert d.getAddressType() == "Bech32-main"
def test_seed_qr():
seed = "121802020768124106400009195602431595117715840445"
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(seed)
assert d.qrType() == QRType.SEEDSSQR
assert d.getSeedPhrase() == "obscure bone gas open exotic abuse virus bunker shuffle nasty ship dash".split()
def test_specter_wallet_json():
parts = [
'p1of3 {"label": "SeedSigner Dev Funds", "blockheight": 692143, "descriptor": "wsh(sortedmulti(4,[e0811b6b/48h/0h/0h/2h]xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt/0/*,[852b308f/48h/0h/0h/2h]xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh/0/*,[7edf9c59/48h/0h/0h/2h]xpub6DaFfKoe7Wpofr'
, 'p2of3 bYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs/0/*,[b433e095/48h/0h/0h/2h]xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS/0/*,[184d07eb/48h/0h/0h/2h]xpub6EEoTpcQu7N4R8D84pJjZ69j3mi'
, 'p3of3 nevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy/0/*,[3e451efe/48h/0h/0h/2h]xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g/0/*))#c44hel9e", "devices": [{"type": "other", "label": "Keith"}, {"type": "other", "label": "Nick"}, {"type": "other", "label": "Richard"}, {"type": "other", "label": "Stephan"}, {"type": "other", "label": "SeedSigner 1"}, {"type": "other", "label": "SeedSigner 2"}]}'
]
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(parts[1])
d.addString(parts[2])
d.addString(parts[0])
assert d.qrType() == QRType.SPECTERWALLETQR
assert d.isComplete() == True
assert d.getWalletDescriptor() == "wsh(sortedmulti(4,[e0811b6b/48h/0h/0h/2h]xpub6E8v7uy63pCeJvHe5W8ea8zTnCtKMFgMRb5bueWWcUFMw6sWmUwTqxM8cFiKQRWkA2Fxth9HJZufJwjWTTvU1UGZNpTrh9khrswYMgeHiCt/0/*,[852b308f/48h/0h/0h/2h]xpub6ErhgAWfnEqW7xDBm1iLq5JjNyUS65YUFnjHLrRv9zmdDEtuE75bpWQ8o6bSBnpT6AkrrsA8eA5SmEFArZn11KEPaZJzx9mHTXPWZCsxLyh/0/*,[7edf9c59/48h/0h/0h/2h]xpub6DaFfKoe7WpofrbYeNo3Wv2AiLUMeyrPwotXfukFxUHbK4JxaLHTd5394QtH5wnjFzBgr2YnJpHhXv25Zsqv2APmMFvH1DsKHj5LCr3pmXs/0/*,[b433e095/48h/0h/0h/2h]xpub6EF51itHko2YhGTjVeuYbBgJjVbTzzpYzn2a3JwZHpDrMePRVgXGBHMx2Yv1KwgLsUn9i7ExcAo8uqMx4pDjVRY9J7qnceFAwRRj16dd5AS/0/*,[184d07eb/48h/0h/0h/2h]xpub6EEoTpcQu7N4R8D84pJjZ69j3minevnYLDDoo2HBzYBXTQ4rGVf4XGTyCYFwJuZdsF9MyFYJNzYEjg5LGMA1ubTGWuDnjHAZz6ficVRDTSy/0/*,[3e451efe/48h/0h/0h/2h]xpub6ExQPvQxGBMaPxr8Fv7Vq91ztJFFX3VWvtpvex6UPZ1AptTeuAiJGCtKkgwJkrwpMZMagh9ex6rL4sM8axfFcdQbERoFCRUKTJxrBkJh56g/0/*))#c44hel9e"
def test_specter_wallet_json2():
1_part = '{"label": "Testnet Single Zone", "blockheight": 2090512, "descriptor": "wpkh([990a73ad/84h/1h/0h]tpubDDHQMDnFdan2GyHBsG32VW9qiygbhVizGRTjiS3H79M49FSvpsvLXqLgp1yC7r43dXVHozWavi2Fc4WHUpZmQYmzoQbit28qJhLjScbAQWU/0/*)#ujr0xunp","devices": [{"type": "seedsigner", "label": "Single Seed Zone Testnet"}]}'
d = DecodeQR(wordlist=bip39.WORDLIST)
d.addString(1_part)
assert d.qrType() == QRType.SPECTERWALLETQR
assert d.isComplete() == True