async def failure(self, msg, exc=None, title='Failure'): self.failed = msg self.done() # show line number and/or simple text about error if exc: print("%s:" % msg) sys.print_exception(exc) msg += '\n\n' em = str(exc) if em: msg += em msg += '\n\n' msg += problem_file_line(exc) from main import hsm_active, dis # do nothing more for HSM case: msg will be available over USB if hsm_active: dis.progress_bar(1) # finish the Validating... or whatever was up return # may be a user-abort waiting, but we want to see error msg; so clear it ux_clear_keys(True) return await ux_show_story(msg, title)
async def failure(self, msg, exc=None, title='Failure'): self.failed = msg self.done() if exc: print("%s:" % msg) sys.print_exception(exc) msg += "\n\n(%s)" % problem_file_line(exc) # may be a user-abort waiting, but we want to see error msg; so clear it ux_clear_keys(True) return await ux_show_story(msg, title)
async def start_hsm_approval(sf_len=0, usb_mode=False, startup_mode=False): # Show details of the proposed HSM policy (or saved one) # If approved, go into HSM mode and never come back to normal. UserAuthorizedAction.cleanup() is_new = True if sf_len: with SFFile(0, length=sf_len) as fd: json = fd.read(sf_len).decode() else: try: json = open(POLICY_FNAME, 'rt').read() except: raise ValueError("No existing policy") is_new = False # parse as JSON cant_fail = False try: try: js_policy = ujson.loads(json) except: raise ValueError("JSON parse fail") cant_fail = bool(js_policy.get('boot_to_hsm', False)) # parse the policy policy = HSMPolicy() policy.load(js_policy) except BaseException as exc: err = "HSM Policy invalid: %s: %s" % (problem_file_line(exc), str(exc)) if usb_mode: raise ValueError(err) # What to do in a menu case? Shouldn't happen anyway, but # maybe they upgraded the firmware, and so old policy file # isn't suitable anymore. # - or maybe the settings have been f-ed with. print(err) if startup_mode and cant_fail: # die as a brick here, not safe to proceed w/o HSM active import callgate, ux ux.show_fatal_error(err.replace(': ', ':\n ')) callgate.show_logout(1) # die w/ it visible # not reached await ux_show_story("Cannot start HSM.\n\n%s" % err) return # Boot-to-HSM feature: don't ask, just start policy immediately if startup_mode and policy.boot_to_hsm: msg = uio.StringIO() policy.explain(msg) policy.activate(False) the_ux.reset(hsm_ux_obj) return None ar = ApproveHSMPolicy(policy, is_new) UserAuthorizedAction.active_request = ar if startup_mode: return ar if usb_mode: # for USB case, kill any menu stack, and put our thing at the top abort_and_goto(UserAuthorizedAction.active_request) else: # menu item case: add to stack, so we can still back out from ux import the_ux the_ux.push(UserAuthorizedAction.active_request) return ar
async def usb_hid_recv(self): # blocks and builds up a full-length command packet in memory # - calls self.handle() once complete msg on hand msg_len = 0 while 1: yield IORead(self.blockable) try: here, is_last, is_encrypted = self.get_packet() #print('Rx[%d]' % len(here)) if here: lh = len(here) if msg_len + lh > MAX_MSG_LEN: raise FramingError('xlong') self.msg[msg_len:msg_len + lh] = here msg_len += lh else: # treat zero-length packets as a reset request # do not echo anything back on link.. used to resync connection msg_len = 0 continue if not is_last: # need more content continue if not (4 <= msg_len <= MAX_MSG_LEN): raise FramingError('badsz') if is_encrypted: if self.decrypt is None: raise FramingError('no key') self.encrypted_req = True self.decrypt_inplace(msg_len) else: self.encrypted_req = False # process request try: # this saves memory over a simple slice (confirmed) args = memoryview(self.msg)[4:msg_len] resp = await self.handle(self.msg[0:4], args) msg_len = 0 except CCBusyError: # auth UX is doing something else resp = b'busy' msg_len = 0 except HSMDenied: resp = b'err_Not allowed in HSM mode' msg_len = 0 except (ValueError, AssertionError) as exc: # some limited invalid args feedback #print("USB request caused assert: ", end='') #sys.print_exception(exc) msg = str(exc) if not msg: msg = 'Assertion ' + problem_file_line(exc) resp = b'err_' + msg.encode()[0:80] msg_len = 0 except Exception as exc: # catch bugs and fuzzing too print("USB request caused this: ", end='') sys.print_exception(exc) resp = b'err_Confused ' + problem_file_line(exc) msg_len = 0 # aways send a reply if they get this far await self.send_response(resp) except FramingError as exc: reason = exc.args[0] print("Framing: %s" % reason) self.framing_error(reason) msg_len = 0 except BaseException as exc: # recover from general issues/keep going print("USB!") sys.print_exception(exc) msg_len = 0
async def approve_transaction(self, psbt, psbt_sha, story): # Approve or don't a transaction. Catch assertions and other # reasons for failing/rejecting into the log. # - return 'y' or 'x' chain = chains.current_chain() assert psbt_sha and len(psbt_sha) == 32 self.get_time_left() with AuditLogger('psbt', psbt_sha, self.never_log) as log: if self.must_log and log.is_unsaved: self.refuse(log, "Could not log details, and must_log is set") return 'x' log.info('Transaction signing requested:') log.info('SHA256(PSBT) = ' + b2a_hex(psbt_sha).decode('ascii')) log.info('-vvv-\n%s\n-^^^-' % story) # reset pending auth list and "consume" it now auth = self.pending_auth self.pending_auth = {} try: # do this super early so always cleared even if other issues local_ok = self.consume_local_code(psbt_sha) if not self.rules: raise ValueError("no txn signing allowed") # reject anything with warning, probably if psbt.warnings: if self.warnings_ok: log.info( "Txn has warnings, but policy is to accept anyway." ) else: raise ValueError("has %d warning(s)" % len(psbt.warnings)) # See who has entered creditials already (all must be valid). users = [] for u, (token, counter) in auth.items(): problem = Users.auth_okay(u, token, totp_time=counter, psbt_hash=psbt_sha) if problem: self.refuse( log, "User '%s' gave wrong auth value: %s" % (u, problem)) return 'x' users.append(u) # was right code provided locally? (also resets for next attempt) if local_ok: log.info("Local operator gave correct code.") if users: log.info("These users gave correct auth codes: " + ', '.join(users)) # Where is it going? total_out = 0 dests = [] for idx, tx_out in psbt.output_iter(): if not psbt.outputs[idx].is_change: total_out += tx_out.nValue dests.append(chain.render_address(tx_out.scriptPubKey)) # Pick a rule to apply to this specific txn reasons = [] for rule in self.rules: try: if rule.matches_transaction(psbt, users, total_out, dests, local_ok): break except BaseException as exc: # let's not share these details, except for debug; since # they are not errors, just picking best rule in priority order r = "rule #%d: %s" % (rule.index, str(exc) or problem_file_line(exc)) reasons.append(r) print(r) else: err = "Rejected: " + ', '.join(reasons) self.refuse(log, err) return 'x' if users: msg = ', '.join(auth.keys()) if local_ok: msg += ', and the local operator.' if msg else 'local operator' # looks good, do it self.approve(log, "Acceptable by rule #%d" % rule.index) if rule.per_period is not None: self.record_spend(rule, total_out) return 'y' except BaseException as exc: sys.print_exception(exc) err = "Rejected: " + (str(exc) or problem_file_line(exc)) self.refuse(log, err) return 'x'
async def interact(self): # Prompt user w/ details and get approval from main import dis # step 1: parse PSBT from sflash into in-memory objects. dis.fullscreen("Validating...") try: with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len) as fd: self.psbt = psbtObject.read_psbt(fd) except BaseException as exc: sys.print_exception(exc) if isinstance(exc, MemoryError): msg = "Transaction is too complex." else: msg = "PSBT parse failed" return await self.failure(msg) # Do some analysis/ validation try: await self.psbt.validate() # might do UX: accept multisig import self.psbt.consider_inputs() self.psbt.consider_keys() self.psbt.consider_outputs() except FraudulentChangeOutput as exc: print('FraudulentChangeOutput: ' + exc.args[0]) return await self.failure(exc.args[0], title='Change Fraud') except FatalPSBTIssue as exc: print('FatalPSBTIssue: ' + exc.args[0]) return await self.failure(exc.args[0]) except BaseException as exc: sys.print_exception(exc) del self.psbt gc.collect() if isinstance(exc, MemoryError): msg = "Transaction is too complex." else: msg = "Invalid PSBT: %s (%s)" % (exc, problem_file_line(exc)) return await self.failure(msg, exc) # step 2: figure out what we are approving, so we can get sign-off # - outputs, amounts # - fee # # notes: # - try to handle lots of outputs # - cannot calc fee as sat/byte, only as percent # - somethings are 'warnings': # - fee too big # - inputs we can't sign (no key) # try: msg = uio.StringIO() # mention warning at top wl= len(self.psbt.warnings) if wl == 1: msg.write('(1 warning below)\n\n') elif wl >= 2: msg.write('(%d warnings below)\n\n' % wl) self.output_summary_text(msg) gc.collect() fee = self.psbt.calculate_fee() if fee is not None: msg.write("\nNetwork fee:\n%s %s\n" % self.chain.render_value(fee)) if self.psbt.warnings: msg.write('\n---WARNING---\n\n') for label,m in self.psbt.warnings: msg.write('- %s: %s\n\n' % (label, m)) msg.write("\nPress OK to approve and sign transaction. X to abort.") ch = await ux_show_story(msg, title="OK TO SEND?") except MemoryError as exc: # recovery? maybe. try: del self.psbt del msg except: pass # might be NameError since we don't know how far we got gc.collect() msg = "Transaction is too complex." return await self.failure(msg, exc) if ch != 'y': # they don't want to! self.refused = True await ux_dramatic_pause("Refused.", 1) del self.psbt self.done() return # do the actual signing. try: gc.collect() self.psbt.sign_it() except FraudulentChangeOutput as exc: print('FraudulentChangeOutput: ' + exc.args[0]) return await self.failure(exc.args[0], title='Change Fraud') except MemoryError as exc: msg = "Transaction is too complex." return await self.failure(msg, exc) except BaseException as exc: sys.print_exception(exc) return await self.failure("Signing failed late: %s" % exc) if self.approved_cb: # for micro sd case await self.approved_cb(self.psbt) self.done() return try: # re-serialize the PSBT back out with SFFile(TXN_OUTPUT_OFFSET, max_size=MAX_TXN_LEN, message="Saving...") as fd: await fd.erase() if self.do_finalize: self.psbt.finalize(fd) else: self.psbt.serialize(fd) self.result = (fd.tell(), fd.checksum.digest()) self.done() except BaseException as exc: self.failed = "PSBT output failed" print("PSBT output failure: ") sys.print_exception(exc) self.done() return