def validate_href(self, key, href): """ This function validates that the url is valid. It also prefixes the http:// schema to the url if it was not passed in by the user. """ if len(href) > 2000: raise AppError(f"Url too long in length. Limit: 2000 characters, Length: {len(href)}.") split_url = urlparse(href) # this blog post describes in detail the following regex # https://medium.com/@vaghasiyaharryk/9ab484a1b430 # Summary of rules: # Valid url cannot start or end with - # The valid chars are in range [A-Za-z0-9-] # and there must be between 1 and 63 characters # finally restrict the tld to between 2 and 6 chars if not re.match(r"^((?!-)[A-Za-z0–9-]{1,63}(?<!-)\.)+[A-Za-z]{2,6}$", split_url.netloc): raise AppError(f"Invalid url: {href}") # For now I've decided to reject raw IPv4 and IPv6 addresses, and localhost return href
async def process_host_command(self, stream, show_screen): """ If command with one of the prefixes is received it will be passed to this method. Should return a tuple: - stream (file, BytesIO etc) - meta object with title and note """ # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix == b"getlabel": label = self.get_label() obj = { "title": "Device's label is: %s" % label, } stream = BytesIO(label.encode()) return stream, obj elif prefix == b"setlabel": label = stream.read().strip().decode('ascii') if not label: raise AppError('Device label cannot be empty') scr = Prompt("\n\nSet device label to: %s\n" % label, "Current device label: %s" % self.get_label()) res = await show_screen(scr) if res is False: return None self.set_label(label) obj = { "title": "New device label: %s" % label, } return BytesIO(label.encode()), obj else: raise AppError("Invalid command")
async def process_host_command(self, stream, show_fn): """ If command with one of the prefixes is received it will be passed to this method. Should return a tuple: - stream (file, BytesIO etc) - meta object with title and note """ # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix != b"getrandom": # WTF? It's not our data... raise AppError("Prefix is not valid: %s" % prefix.decode()) # by default we return 32 bytes num_bytes = 32 try: num_bytes = int(stream.read().decode().strip()) except: pass if num_bytes < 0: raise AppError("Seriously? %d bytes? No..." % num_bytes) if num_bytes > 1000: raise AppError("Sorry, 1k bytes max.") obj = {"title": "Here is your entropy", "note": "%d bytes" % num_bytes} return BytesIO(hexlify(get_random_bytes(num_bytes))), obj
async def show_xpub(self, derivation, show_screen): self.show_loader(title="Deriving the key...") derivation = derivation.rstrip("/") net = NETWORKS[self.network] xpub = self.keystore.get_xpub(derivation) ver = bip32.detect_version(derivation, default="xpub", network=net) canonical = xpub.to_base58(net["xpub"]) slip132 = xpub.to_base58(ver) if slip132 == canonical: slip132 = None fingerprint = hexlify(self.keystore.fingerprint).decode() prefix = "[%s%s]" % ( fingerprint, derivation[1:], ) res = await show_screen( XPubScreen(xpub=canonical, slip132=slip132, prefix=prefix)) if res: fname = "%s-%s.txt" % (fingerprint, derivation[2:].replace( "/", "-")) if not platform.is_sd_present(): raise AppError("SD card is not present") platform.mount_sdcard() with open(platform.fpath("/sd/%s" % fname), "w") as f: f.write(res) platform.unmount_sdcard() await show_screen( Alert("Saved!", "Extended public key is saved to the file:\n\n%s" % fname, button_text="Close"))
async def process_host_command(self, stream, show_screen): """ If command with one of the prefixes is received it will be passed to this method. Should return a tuple: - stream (file, BytesIO etc) - meta object with title and note """ # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix != b"hello": # WTF? It's not our data... raise AppError("Prefix is not valid: %s" % prefix.decode()) name = stream.read().decode() # ask the user if he really wants it # build a screen scr = Prompt( "Say hello?", "Are you sure you want to say hello to\n\n%s?\n\n" "Saying hello can compromise your security!" % name) # show screen and wait for result res = await show_screen(scr) # check if he confirmed if not res: return obj = { "title": "Hello!", } d = b"Hello " + name.encode() return BytesIO(d), obj
async def process_host_command(self, stream, show_fn): # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix not in self.prefixes: # WTF? It's not our data... raise AppError("Prefix is not valid: %s" % prefix.decode()) mnemonic = stream.read().strip().decode() if not bip39.mnemonic_is_valid(mnemonic): raise AppError("Invalid mnemonic!") scr = Prompt("Load this mnemonic to memory?", "Mnemonic:") table = MnemonicTable(scr) table.align(scr.message, lv.ALIGN.OUT_BOTTOM_MID, 0, 30) table.set_mnemonic(mnemonic) if await show_fn(scr): self.keystore.set_mnemonic(mnemonic) return None
def write_file(self, filename, filedata): if not platform.is_sd_present(): raise AppError("SD card is not present") platform.mount_sdcard() with open(platform.fpath("/sd/%s" % filename), "w") as f: f.write(filedata) platform.unmount_sdcard()
async def process_host_command(self, stream, show_screen): if self.keystore.is_locked: raise AppError("Device is locked") # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix == b"slip77": if not await show_screen( Prompt( "Confirm the action", "Send master blinding private key\nto the host?\n\n" "Host is requesting your\nSLIP-77 blinding key.\n\n" "It will be able to watch your funds and unblind transactions." )): return return BytesIO(self.keystore.slip77_key.wif( NETWORKS[self.network])), {} raise AppError("Unknown command")
async def process_host_command(self, stream, show_screen): """ If command with one of the prefixes is received it will be passed to this method. Should return a tuple: - stream (file, BytesIO etc) - meta object with title and note """ # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) if prefix != b"signmessage": # WTF? It's not our data... raise AppError("Prefix is not valid: %s" % prefix.decode()) # data format: message to sign<space>derivation_path # read all and delete all crap at the end (if any) # also message should be utf-8 decodable data = stream.read().strip().decode() if " " not in data: raise AppError("Invalid data encoding") arr = data.split(" ") derivation_path = arr[-1] message = " ".join(arr[:-1]) # if we have fingerprint if not derivation_path.startswith("m/"): fingerprint = unhexlify(derivation_path[:8]) if fingerprint != self.keystore.fingerprint: raise AppError("Not my fingerprint") derivation_path = "m" + derivation_path[8:] derivation_path = bip32.parse_path(derivation_path) # ask the user if he really wants to sign this message scr = Prompt( "Sign message with private key at %s?" % bip32.path_to_str(derivation_path), "Message:\n%s" % message) res = await show_screen(scr) if res is False: return None sig = self.sign_message(derivation_path, message.encode()) # for GUI we can also return an object with helpful data xpub = self.keystore.get_xpub(derivation_path) address = script.p2pkh(xpub.key).address() obj = { "title": "Signature for the message:", "note": "using address %s" % address } return BytesIO(sig), obj
def wrapped_function(*args, **kwargs): if request.method in ['POST', 'DELETE']: data = request.get_data(cache=False) if not data: raise AppError('No data was POSTed', 400) try: request_charset = request.mimetype_params.get('charset') if request_charset is not None: data = json.loads(data, encoding=request_charset) else: data = json.loads(data) except: raise AppError('Unable to parse POSTed JSON', 400) request.data = data return fn(*args, **kwargs)
def parse_software_wallet_json(obj): """Parse software export json""" if "descriptor" not in obj: raise AppError("Invalid wallet json") # get descriptor without checksum desc = obj["descriptor"].split("#")[0] # replace /0/* to /{0,1}/* to add change descriptor desc = desc.replace("/0/*", "/{0,1}/*") label = obj.get("label", "Imported wallet") return label, desc
async def process_host_command(self, stream, show_screen): if self.keystore.is_locked: raise AppError("Device is locked") # reads prefix from the stream (until first space) prefix = self.get_prefix(stream) # get device fingerprint, data is ignored if prefix == b"fingerprint": return BytesIO(hexlify(self.keystore.fingerprint)), {} # get xpub, # data: derivation path in human-readable form like m/44h/1h/0 elif prefix == b"xpub": try: path = stream.read().strip() # convert to list of indexes path = bip32.parse_path(path.decode()) except: raise AppError('Invalid path: "%s"' % path.decode()) # get xpub xpub = self.keystore.get_xpub(bip32.path_to_str(path)) # send back as base58 return BytesIO(xpub.to_base58(NETWORKS[self.network]["xpub"]).encode()), {} raise AppError("Unknown command")
async def process_host_command(self, stream, show_fn): # check if we've got filename, not a stream: if isinstance(stream, str): with open(stream, "rb") as f: return await self.process_host_command(f, show_fn) # processing stream now c = stream.read(16) # rewind stream.seek(-len(c), 1) if c.startswith(b"{"): obj = json.load(stream) if "descriptor" in obj: # this is wallet export json (Specter Desktop, FullyNoded and others) return await self.parse_software_wallet_json(obj, show_fn) elif c.startswith(b"#") or c.startswith(b"Name:"): return await self.parse_cc_wallet_txt(stream, show_fn) raise AppError("Failed parsing data")
async def menu(self, show_screen, show_all=False): net = NETWORKS[self.network] coin = net["bip32"] buttons = [ (None, "Recommended"), ("m/84h/%dh/%dh" % (coin, self.account), "Single key"), ("m/48h/%dh/%dh/2h" % (coin, self.account), "Multisig"), (None, "Other keys"), ] if show_all: buttons += [ ("m/84h/%dh/%dh" % (coin, self.account), "Single Native Segwit\nm/84h/%dh/%dh" % (coin, self.account)), ("m/49h/%dh/%dh" % (coin, self.account), "Single Nested Segwit\nm/49h/%dh/%dh" % (coin, self.account)), ( "m/48h/%dh/%dh/2h" % (coin, self.account), "Multisig Native Segwit\nm/48h/%dh/%dh/2h" % (coin, self.account), ), ( "m/48h/%dh/%dh/1h" % (coin, self.account), "Multisig Nested Segwit\nm/48h/%dh/%dh/1h" % (coin, self.account), ), ] else: buttons += [(0, "Show more keys"), (2, "Change account number"), (1, "Enter custom derivation")] # wait for menu selection menuitem = await show_screen( Menu(buttons, last=(255, None), title="Select the key", note="Current account number: %d" % self.account)) # process the menu button: # back button if menuitem == 255: return False elif menuitem == 0: return await self.menu(show_screen, show_all=True) elif menuitem == 1: der = await show_screen(DerivationScreen()) if der is not None: await self.show_xpub(der, show_screen) return True elif menuitem == 2: account = await show_screen( NumericScreen(current_val=str(self.account))) if account and int(account) > 0x80000000: raise AppError('Account number too large') try: self.account = int(account) except: self.account = 0 return await self.menu(show_screen) else: await self.show_xpub(menuitem, show_screen) return True return False
def set_label(self, label): try: with open(self.path + "/label", "w") as f: f.write(label) except Exception: return AppError("Failed to save new label")
async def menu(self, show_screen, show_all=False): net = NETWORKS[self.network] coin = net["bip32"] if not show_all: buttons = [ (None, "Recommended"), ("m/84h/%dh/%dh" % (coin, self.account), "Single key"), ("m/48h/%dh/%dh/2h" % (coin, self.account), "Multisig"), (None, "Other keys"), (0, "Show more keys"), (2, "Change account number"), (1, "Enter custom derivation"), (3, "Export all keys to SD"), ] else: buttons = [ (None, "Recommended"), ( "m/84h/%dh/%dh" % (coin, self.account), "Single Native Segwit\nm/84h/%dh/%dh" % (coin, self.account) ), ( "m/48h/%dh/%dh/2h" % (coin, self.account), "Multisig Native Segwit\nm/48h/%dh/%dh/2h" % (coin, self.account), ), (None, "Other keys"), ] if self.is_taproot_enabled: buttons.append(( "m/86h/%dh/%dh" % (coin, self.account), "Single Taproot\nm/86h/%dh/%dh" % (coin, self.account) )) buttons.extend([ ( "m/49h/%dh/%dh" % (coin, self.account), "Single Nested Segwit\nm/49h/%dh/%dh" % (coin, self.account) ), ( "m/48h/%dh/%dh/1h" % (coin, self.account), "Multisig Nested Segwit\nm/48h/%dh/%dh/1h" % (coin, self.account), ), ]) # wait for menu selection menuitem = await show_screen(Menu(buttons, last=(255, None), title="Select the key", note="Current account number: %d" % self.account)) # process the menu button: # back button if menuitem == 255: return False elif menuitem == 0: return await self.menu(show_screen, show_all=True) elif menuitem == 1: der = await show_screen(DerivationScreen()) if der is not None: await self.show_xpub(der, show_screen) return True elif menuitem == 3: file_format = await self.save_menu(show_screen) if file_format: filename = self.save_all_to_sd(file_format) await show_screen( Alert("Saved!", "Public keys are saved to the file:\n\n%s" % filename, button_text="Close") ) elif menuitem == 2: account = await show_screen(NumericScreen(current_val=str(self.account))) if account and int(account) > 0x80000000: raise AppError('Account number too large') try: self.account = int(account) except: self.account = 0 return await self.menu(show_screen) else: await self.show_xpub(menuitem, show_screen) return True return False
def parse_cc_wallet_txt(stream): """Parse coldcard wallet format""" name = "Imported wallet" script_type = None sigs_required = None global_derivation = None sigs_total = None cosigners = [] current_derivation = None # cycle until we read everything char = b"\n" while char is not None: line, char = read_until(stream, b"\r\n", max_len=300) # skip comments while char is not None and (line.startswith(b"#") or len(line.strip()) == 0): # BW comment on derivation if line.startswith(b"# derivation:"): current_derivation = bip32.parse_path( line.split(b":")[1].decode().strip()) line, char = read_until(stream, b"\r\n", max_len=300) if b":" not in line: continue arr = line.split(b":") if len(arr) > 2: raise AppError("Invalid file format") k, v = [a.strip().decode() for a in arr] if k == "Name": name = v elif k == "Policy": nums = [int(num) for num in v.split(" of ")] assert len(nums) == 2 m, n = nums assert m > 0 and n >= m sigs_required = m sigs_total = n elif k == "Format": assert v in CC_TYPES script_type = CC_TYPES[v] elif k == "Derivation": der = bip32.parse_path(v) if len(cosigners) == 0: global_derivation = der else: current_derivation = der # fingerprint elif len(k) == 8: cosigners.append( (unhexlify(k), current_derivation or global_derivation, bip32.HDKey.from_string(v))) current_derivation = None assert None not in [ global_derivation, sigs_total, sigs_required, script_type, name ] assert len(cosigners) == sigs_total xpubs = [ "[%s]%s/{0,1}/*" % (bip32.path_to_str(der, fingerprint=fgp), xpub) for fgp, der, xpub in cosigners ] desc = "sortedmulti(%d,%s)" % (sigs_required, ",".join(xpubs)) for sc in reversed(script_type.split("-")): desc = "%s(%s)" % (sc, desc) return name, desc