async def test_7z(): # test full 7z round-trip # Altho cleartext mode is not for real, if the code is written, I must test it. from backups import write_complete_backup, restore_complete_doit from sffile import SFFile import tcc from main import settings, sf, numpad today = tcc.random.uniform(1000000) import machine machine.reset = lambda: None for chain in ['BTC', 'XTN']: for words in ([], ['abc', 'def']): settings.set('check', today) settings.set('chain', chain) ll, sha = await write_complete_backup(words, None, True) result = SFFile(0, ll).read() if words: #open('debug.7z', 'wb').write(result) assert ll > 800 assert len(sha) == 32 assert result[0:6] == b"7z\xbc\xaf'\x1c" assert tcc.sha256(result).digest() == sha assert len(set(result)) >= 240 # encrypted else: sr = str(result, 'ascii') print("Backup contents:\n" + sr) assert sr[0] == '#', result assert 'Coldcard' in sr assert len(set(sr)) < 100 # cleartext, english assert ('chain = "%s"' % chain) in result # test restore # - cant wipe flash, since the backup file is there # - cant wipe all settings becuase PIN and stuff is simulated there del settings.current['check'] with SFFile(0, ll) as fd: numpad.inject('y') # for 'success' message await restore_complete_doit(fd, words) assert settings.get('check') == today, \ (settings.get('check'), '!=', today) assert settings.get('chain') == chain, \ (settings.get('chain'), '!=', chain) today += 3 import ux ux.restore_menu()
def maybe_enroll_xpub(sf_len=None, config=None, name=None, ux_reset=False): # Offer to import (enroll) a new multisig wallet. Allow reject by user. global active_request from multisig import MultisigWallet UserAuthorizedAction.cleanup() if sf_len: with SFFile(TXN_INPUT_OFFSET, length=sf_len) as fd: config = fd.read(sf_len).decode() # this call will raise on parsing errors, so let them rise up # and be shown on screen/over usb ms = MultisigWallet.from_file(config, name=name) active_request = NewEnrollRequest(ms) if ux_reset: # for USB case, and import from PSBT # kill any menu stack, and put our thing at the top abort_and_goto(active_request) else: # menu item case: add to stack from ux import the_ux the_ux.push(active_request)
def save_visualization(self, msg, sign_text=False): # write text into spi flash, maybe signing it as we go # - return length and checksum txt_len = msg.seek(0, 2) msg.seek(0) chk = self.chain.hash_message(msg_len=txt_len) if sign_text else None with SFFile(TXN_OUTPUT_OFFSET, max_size=txt_len+300, message="Visualizing...") as fd: await fd.erase() while 1: blk = msg.read(256).encode('ascii') if not blk: break if chk: chk.update(blk) fd.write(blk) if chk: from ubinascii import b2a_base64 # append the signature digest = tcc.sha256(chk.digest()).digest() sig = sign_message_digest(digest, 'm', None) fd.write(b2a_base64(sig).decode('ascii').strip()) fd.write('\n') return (fd.tell(), fd.checksum.digest())
def save(self): # render as JSON, encrypt and write it. self.current['_age'] = self.current.get('_age', 1) + 1 pos = self.find_spot(self.my_pos) aes = self.get_aes(pos).cipher with SFFile(pos, max_size=4096, pre_erased=True) as fd: chk = sha256() # first the json data d = ujson.dumps(self.current) # pad w/ zeros dat_len = len(d) pad_len = (4096 - 32) - dat_len assert pad_len >= 0, 'too big' self.capacity = dat_len / 4096 fd.write(aes(d)) chk.update(d) del d while pad_len > 0: here = min(32, pad_len) pad = bytes(here) fd.write(aes(pad)) chk.update(pad) pad_len -= here fd.write(aes(chk.digest())) assert fd.tell() == 4096 # erase old copy of data if self.my_pos and self.my_pos != pos: SF.wait_done() SF.sector_erase(self.my_pos) SF.wait_done() self.my_pos = pos self.is_dirty = 0
def save(self): # render as JSON, encrypt and write it. self.current['_age'] = self.current.get('_age', 1) + 1 sf, pos = self.find_spot(self.my_pos) aes = self.get_aes(tcc.AES.Encrypt, pos) with SFFile(pos, max_size=4096, pre_erased=True) as fd: chk = tcc.sha256() # first the json data d = ujson.dumps(self.current) fd.write(aes.update(d)) chk.update(d) # pad w/ zeros pad_len = (4096 - 32) - len(d) del d assert pad_len >= 0, 'too big' while pad_len > 0: here = min(32, pad_len) pad = bytes(here) fd.write(aes.update(pad)) chk.update(pad) pad_len -= here fd.write(aes.update(chk.digest())) assert fd.tell() == 4096 # erase old copy of data if self.my_pos and self.my_pos != pos: sf.wait_done() sf.sector_erase(self.my_pos) sf.wait_done() self.my_pos = pos self.is_dirty = 0
def load(self, dis=None): # Search all slots for any we can read, decrypt that, # and pick the newest one (in unlikely case of dups) # reset self.current.clear() self.overrides.clear() self.my_pos = 0 self.is_dirty = 0 self.capacity = 0 # 4k, but last 32 bytes are a SHA (itself encrypted) global _tmp buf = bytearray(4) empty = 0 for pos in SLOTS: if dis: dis.progress_bar_show( (pos - SLOTS.start) / (SLOTS.stop - SLOTS.start)) gc.collect() SF.read(pos, buf) if buf[0] == buf[1] == buf[2] == buf[3] == 0xff: # erased (probably) empty += 1 continue # check if first 2 bytes makes sense for JSON aes = self.get_aes(pos) chk = aes.copy().cipher(b'{"') if chk != buf[0:2]: # doesn't look like JSON meant for me continue # probably good, read it chk = sha256() aes = aes.cipher expect = None with SFFile(pos, length=4096, pre_erased=True) as fd: for i in range(4096 / 32): b = aes(fd.read(32)) if i != 127: _tmp[i * 32:(i * 32) + 32] = b chk.update(b) else: expect = b try: # verify checksum in last 32 bytes assert expect == chk.digest() # loads() can't work from a byte array, and converting to # bytes here would copy it; better to use file emulation. fd = BytesIO(_tmp) d = ujson.load(fd) self.capacity = fd.seek(0, 1) / 4096 # .tell() is missing except: # One in 65k or so chance to come here w/ garbage decoded, so # not an error. continue got_age = d.get('_age', 0) if got_age > self.current.get('_age', -1): # likely winner self.current = d self.my_pos = pos #print("NV: data @ %d w/ age=%d" % (pos, got_age)) else: # stale data seen; clean it up. assert self.current['_age'] > 0 #print("NV: cleanup @ %d" % pos) SF.sector_erase(pos) SF.wait_done() # 4k is a large object, sigh, for us right now. cleanup gc.collect() # done, if we found something if self.my_pos: return # nothing found. self.my_pos = 0 self.current = self.default_values() if empty == len(SLOTS): # Whole thing is blank. Bad for plausible deniability. Write 3 slots # with garbage. They will be wasted space until it fills. blks = list(SLOTS) shuffle(blks) for pos in blks[0:3]: for i in range(0, 4096, 256): h = ngu.random.bytes(256) SF.wait_done() SF.write(pos + i, h)
async def write_complete_backup(words, fname_pattern, write_sflash): # Just do the writing from main import dis, pa, settings from files import CardSlot, CardMissingError # Show progress: dis.fullscreen('Encrypting...' if words else 'Generating...') body = render_backup_contents().encode() gc.collect() if words: # NOTE: Takes a few seconds to do the key-streching, but little actual # time to do the encryption. pw = ' '.join(words) zz = compat7z.Builder(password=pw, progress_fcn=dis.progress_bar_show) zz.add_data(body) hdr, footer = zz.save('ckcc-backup.txt') filesize = len(body) + MAX_BACKUP_FILE_SIZE del body gc.collect() else: # cleartext dump zz = None filesize = len(body) + 10 if write_sflash: # for use over USB and unit testing: commit file into SPI flash from sffile import SFFile with SFFile(0, max_size=filesize, message='Saving...') as fd: await fd.erase() if zz: fd.write(hdr) fd.write(zz.body) fd.write(footer) else: fd.write(body) return fd.tell(), fd.checksum.digest() for copy in range(25): # choose a filename try: with CardSlot() as card: fname, nice = card.pick_filename(fname_pattern) # do actual write with open(fname, 'wb') as fd: if zz: fd.write(hdr) fd.write(zz.body) fd.write(footer) else: fd.write(body) except Exception as e: # includes CardMissingError import sys sys.print_exception(e) # catch any error ch = await ux_show_story( 'Failed to write! Please insert formated MicroSD card, ' 'and press OK to try again.\n\nX to cancel.\n\n\n' + str(e)) if ch == 'x': break continue if copy == 0: while 1: msg = '''Backup file written:\n\n%s\n\n\ To view or restore the file, you must have the full password.\n\n\ Insert another SD card and press 2 to make another copy.''' % (nice) ch = await ux_show_story(msg, escape='2') if ch == 'y': return if ch == '2': break else: ch = await ux_show_story('''File (#%d) written:\n\n%s\n\n\ Press OK for another copy, or press X to stop.''' % (copy + 1, nice), escape='2') if ch == 'x': break
def sign_psbt_file(filename): # sign a PSBT file found on a MicroSD card from files import CardSlot, CardMissingError from main import dis from sram2 import tmp_buf global active_request UserAuthorizedAction.cleanup() #print("sign: %s" % filename) # copy file into our spiflash # - can't work in-place on the card because we want to support writing out to different card with CardSlot() as card: with open(filename, 'rb') as fd: dis.fullscreen('Reading...') # see how long it is psbt_len = fd.seek(0, 2) fd.seek(0) total = 0 with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out: # blank flash await out.erase() while 1: n = fd.readinto(tmp_buf) if not n: break if n == len(tmp_buf): out.write(tmp_buf) else: out.write(memoryview(tmp_buf)[0:n]) total += n dis.progress_bar_show(total / psbt_len) assert total == psbt_len, repr([total, psbt_len]) async def done(psbt): orig_path, basename = filename.rsplit('/', 1) orig_path += '/' base = basename.rsplit('.', 1)[0] out2_fn = None out_fn = None while 1: # try to put back into same spot, but also do top-of-card is_comp = psbt.is_complete() if not is_comp: # keep the filename under control during multiple passes target_fname = base.replace('-part', '')+'-part.psbt' else: # add -signed to end. We won't offer to sign again. target_fname = base+'-signed.psbt' for path in [orig_path, None]: try: with CardSlot() as card: out_full, out_fn = card.pick_filename(target_fname, path) out_path = path if out_full: break except CardMissingError: prob = 'Missing card.\n\n' out_fn = None if not out_fn: # need them to insert a card prob = '' else: # attempt write-out try: with CardSlot() as card: with open(out_full, 'wb') as fd: # save as updated PSBT psbt.serialize(fd) if is_comp: # write out as hex too, if it's final out2_full, out2_fn = card.pick_filename(base+'-final.txn', out_path) if out2_full: with HexWriter(open(out2_full, 'wt')) as fd: # save transaction, in hex psbt.finalize(fd) # success and done! break except OSError as exc: prob = 'Failed to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? ch = await ux_show_story(prob+"Please insert an SDCard to receive signed transaction, " "and press OK.", title="Need Card") if ch == 'x': await ux_aborted() return # done. msg = "Updated PSBT is:\n\n%s" % out_fn if out2_fn: msg += '\n\nFinalized transaction (ready for broadcast):\n\n%s' % out2_fn await ux_show_story(msg, title='PSBT Signed') UserAuthorizedAction.cleanup() active_request = ApproveTransaction(psbt_len, approved_cb=done) # kill any menu stack, and put our thing at the top abort_and_goto(active_request)
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: if isinstance(exc, MemoryError): msg = "Transaction is too complex" exc = None else: msg = "PSBT parse failed" return await self.failure(msg, exc) # 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: del self.psbt gc.collect() if isinstance(exc, MemoryError): msg = "Transaction is too complex" exc = None else: msg = "Invalid PSBT" 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: # 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) 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: return await self.failure(exc.args[0], title='Change Fraud') except MemoryError: msg = "Transaction is too complex" return await self.failure(msg) except BaseException as exc: return await self.failure("Signing failed late", 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: return await self.failure("PSBT output failed", exc)
def sign_psbt_buf(psbt_buf): # sign a PSBT file found on a microSD card from uio import BytesIO from common import dis from sram4 import tmp_buf from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer UserAuthorizedAction.cleanup() # copy buffer into SPI Flash # - accepts hex or base64 encoding, but binary prefered with BytesIO(psbt_buf) as fd: dis.fullscreen('Reading...') # see how long it is psbt_len = fd.seek(0, 2) fd.seek(0) # determine encoding used, altho we prefer binary taste = fd.read(10) fd.seek(0) if taste[0:5] == b'psbt\xff': print('tastes like text PSBT') decoder = None def output_encoder(x): return x elif taste[0:10] == b'70736274ff': print('tastes like binary PSBT') decoder = HexStreamer() output_encoder = HexWriter psbt_len //= 2 elif taste[0:6] == b'cHNidP': print('tastes like Base64 PSBT') decoder = Base64Streamer() output_encoder = Base64Writer psbt_len = (psbt_len * 3 // 4) + 10 else: return total = 0 with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out: print('sign 1') # blank flash await out.erase() print('sign 2') while 1: n = fd.readinto(tmp_buf) print('sign copy to SPI flash 1: n={}'.format(n)) if not n: break if n == len(tmp_buf): abuf = tmp_buf else: abuf = memoryview(tmp_buf)[0:n] if not decoder: out.write(abuf) total += n else: for here in decoder.more(abuf): out.write(here) total += len(here) print('sign copy to SPI flash 2: {}/{} = {}'.format(total, psbt_len, total/psbt_len)) dis.progress_bar_show(total / psbt_len) print('sign 3') # might have been whitespace inflating initial estimate of PSBT size assert total <= psbt_len psbt_len = total print('sign 4') # Create a new BytesIO() to hold the result async def done(psbt): print('sign 5: done') signed_bytes = None with BytesIO() as bfd: with output_encoder(bfd) as fd: print('sign 6: done') if psbt.is_complete(): print('sign 7: done') psbt.finalize(fd) print('sign 8: done') else: print('sign 9: done') psbt.serialize(fd) print('sign 10: done') bfd.seek(0) signed_bytes = bfd.read() print('signed_bytes={}'.format(signed_bytes)) print('sign 11: done') gc.collect() from ur1.encode_ur import encode_ur from ubinascii import hexlify signed_str = hexlify(signed_bytes) print('signed_str={}'.format(signed_str)) from ux import DisplayURCode o = DisplayURCode('Signed Txn', 'Scan to Wallet', signed_str) await o.interact_bare() UserAuthorizedAction.cleanup() print('sign 12: done') UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, approved_cb=done) print('sign 13: done') # kill any menu stack, and put our thing at the top abort_and_goto(UserAuthorizedAction.active_request) print('sign 14: done')
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
def sign_psbt_file(filename): # sign a PSBT file found on a MicroSD card from files import CardSlot, CardMissingError, securely_blank_file from main import dis from sram2 import tmp_buf from utils import HexStreamer, Base64Streamer, HexWriter, Base64Writer UserAuthorizedAction.cleanup() #print("sign: %s" % filename) # copy file into our spiflash # - can't work in-place on the card because we want to support writing out to different card # - accepts hex or base64 encoding, but binary prefered with CardSlot() as card: with open(filename, 'rb') as fd: dis.fullscreen('Reading...') # see how long it is psbt_len = fd.seek(0, 2) fd.seek(0) # determine encoding used, altho we prefer binary taste = fd.read(10) fd.seek(0) if taste[0:5] == b'psbt\xff': decoder = None output_encoder = lambda x: x elif taste[0:10] == b'70736274ff': decoder = HexStreamer() output_encoder = HexWriter psbt_len //= 2 elif taste[0:6] == b'cHNidP': decoder = Base64Streamer() output_encoder = Base64Writer psbt_len = (psbt_len * 3 // 4) + 10 total = 0 with SFFile(TXN_INPUT_OFFSET, max_size=psbt_len) as out: # blank flash await out.erase() while 1: n = fd.readinto(tmp_buf) if not n: break if n == len(tmp_buf): abuf = tmp_buf else: abuf = memoryview(tmp_buf)[0:n] if not decoder: out.write(abuf) total += n else: for here in decoder.more(abuf): out.write(here) total += len(here) dis.progress_bar_show(total / psbt_len) # might have been whitespace inflating initial estimate of PSBT size assert total <= psbt_len psbt_len = total async def done(psbt): orig_path, basename = filename.rsplit('/', 1) orig_path += '/' base = basename.rsplit('.', 1)[0] out2_fn = None out_fn = None txid = None from main import settings import os del_after = settings.get('del', 0) while 1: # try to put back into same spot, but also do top-of-card is_comp = psbt.is_complete() if not is_comp: # keep the filename under control during multiple passes target_fname = base.replace('-part', '')+'-part.psbt' else: # add -signed to end. We won't offer to sign again. target_fname = base+'-signed.psbt' for path in [orig_path, None]: try: with CardSlot() as card: out_full, out_fn = card.pick_filename(target_fname, path) out_path = path if out_full: break except CardMissingError: prob = 'Missing card.\n\n' out_fn = None if not out_fn: # need them to insert a card prob = '' else: # attempt write-out try: with CardSlot() as card: if is_comp and del_after: # don't write signed PSBT if we'd just delete it anyway out_fn = None else: with output_encoder(open(out_full, 'wb')) as fd: # save as updated PSBT psbt.serialize(fd) if is_comp: # write out as hex too, if it's final out2_full, out2_fn = card.pick_filename( base+'-final.txn' if not del_after else 'tmp.txn', out_path) if out2_full: with HexWriter(open(out2_full, 'w+t')) as fd: # save transaction, in hex txid = psbt.finalize(fd) if del_after: # rename it now that we know the txid after_full, out2_fn = card.pick_filename( txid+'.txn', out_path, overwrite=True) os.rename(out2_full, after_full) if del_after: # this can do nothing if they swapped SDCard between steps, which is ok, # but if the original file is still there, this blows it away. # - if not yet final, the foo-part.psbt file stays try: securely_blank_file(filename) except: pass # success and done! break except OSError as exc: prob = 'Failed to write!\n\n%s\n\n' % exc sys.print_exception(exc) # fall thru to try again # prompt them to input another card? ch = await ux_show_story(prob+"Please insert an SDCard to receive signed transaction, " "and press OK.", title="Need Card") if ch == 'x': await ux_aborted() return # done. if out_fn: msg = "Updated PSBT is:\n\n%s" % out_fn if out2_fn: msg += '\n\n' else: # del_after is probably set msg = '' if out2_fn: msg += 'Finalized transaction (ready for broadcast):\n\n%s' % out2_fn if txid and not del_after: msg += '\n\nFinal TXID:\n'+txid await ux_show_story(msg, title='PSBT Signed') UserAuthorizedAction.cleanup() UserAuthorizedAction.active_request = ApproveTransaction(psbt_len, approved_cb=done) # kill any menu stack, and put our thing at the top abort_and_goto(UserAuthorizedAction.active_request)
# # this will run on the simulator # run manually with: # execfile('../../testing/devtest/unit_psbt.py') from ubinascii import hexlify as b2a_hex from ubinascii import unhexlify as a2b_hex import tcc, ustruct from main import settings from public_constants import MAX_TXN_LEN # load PSBT into simulated SPI Flash from sffile import SFFile wr_fd = SFFile(0, max_size=MAX_TXN_LEN) list(wr_fd.erase()) out_fd = SFFile(MAX_TXN_LEN, max_size=MAX_TXN_LEN) list(out_fd.erase()) # read from into MacOS filesystem import main fname = getattr(main, 'FILENAME', '../../testing/data/2-of-2.psbt') print("Input PSBT: " + fname) is_hex = False tl = 0 with open(fname, 'rb') as orig: while 1: here = orig.read(256) if not here: break
async def interact(self): # Prompt user w/ details and get approval from glob import dis, hsm_active # step 1: parse PSBT from sflash into in-memory objects. try: dis.fullscreen("Reading...") with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len) as fd: self.psbt = psbtObject.read_psbt(fd) except BaseException as exc: if isinstance(exc, MemoryError): msg = "Transaction is too complex" exc = None else: msg = "PSBT parse failed" return await self.failure(msg, exc) dis.fullscreen("Validating...") # 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: del self.psbt gc.collect() if isinstance(exc, MemoryError): msg = "Transaction is too complex" exc = None else: msg = "Invalid PSBT" 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)) # NEW: show where all the change outputs are going self.output_change_text(msg) gc.collect() 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)) if self.do_visualize: # stop here and just return the text of approval message itself self.result = await self.save_visualization(msg, (self.stxn_flags & STXN_SIGNED)) del self.psbt self.done() return if not hsm_active: msg.write("\nPress OK to approve and sign transaction. X to abort.") ch = await ux_show_story(msg, title="OK TO SEND?") else: ch = await hsm_active.approve_transaction(self.psbt, self.psbt_sha, msg.getvalue()) dis.progress_bar(1) # finish the Validating... except MemoryError: # 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) 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: dis.fullscreen('Wait...') gc.collect() # visible delay causes by this but also sign_it() below self.psbt.sign_it() except FraudulentChangeOutput as exc: return await self.failure(exc.args[0], title='Change Fraud') except MemoryError: msg = "Transaction is too complex" return await self.failure(msg) except BaseException as exc: return await self.failure("Signing failed late", exc) if self.approved_cb: # for micro sd case await self.approved_cb(self.psbt) self.done() return txid = None 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: txid = self.psbt.finalize(fd) else: self.psbt.serialize(fd) self.result = (fd.tell(), fd.checksum.digest()) self.done(redraw=(not txid)) except BaseException as exc: return await self.failure("PSBT output failed", exc) if self.do_finalize and txid and not hsm_active: # Show txid when we can; advisory # - maybe even as QR, hex-encoded in alnum mode tmsg = txid if version.has_fatram: tmsg += '\n\nPress 1 for QR Code of TXID.' ch = await ux_show_story(tmsg, "Final TXID", escape='1') if version.has_fatram and ch=='1': await show_qr_code(txid, True)
# (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # # work thru the first example given in BIP-143 from h import a2b_hex, b2a_hex from psbt import psbtObject, psbtInputProxy, psbtOutputProxy from serializations import CTxIn from uio import BytesIO from sffile import SFFile # NOTE: not a psbt, just a txn # - 2 ins, 2 outs unsigned = a2b_hex('0100000002fff7f7881a8099afa6940d42d1e7f6362bec38171ea3edf433541db4e4ad969f0000000000eeffffffef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff02202cb206000000001976a9148280b37df378db99f66f85c95a783a76ac7a6d5988ac9093510d000000001976a9143bde42dbee7e4dbe6a21b2d50ce2f0167faa815988ac11000000') fd = SFFile(0, max_size=65536) list(fd.erase()) fd.write(b'psbt\xff\x01\x00' + bytes([len(unsigned)]) + unsigned + (b'\0'*8)) psbt_len = fd.tell() rfd = SFFile(0, psbt_len) p = psbtObject.read_psbt(rfd) #p.validate() # failed because no subpaths; don't care amt = 600000000 sc = a2b_hex('1976a9141d0f172a0ecb48aee1be1f2687d2963ae33f71a188ac') outpt2 = a2b_hex('ef51e1b804cc89d182d279655c3aa89e815b1b309fe287d9b2b55d57b90ec68a0100000000ffffffff') replacement = CTxIn()
async def test_7z(): # test full 7z round-trip # Altho cleartext mode is not for real, if the code is written, I must test it. from backups import write_complete_backup, restore_complete_doit from sffile import SFFile import ngu, version, uos from glob import numpad from pincodes import pa from nvstore import settings if version.has_fatram: import hsm had_policy = hsm.hsm_policy_available() else: had_policy = False today = ngu.random.uniform(1000000) import machine machine.reset = lambda: None for chain in ['BTC', 'XTN']: for words in ([], ['abc', 'def']): settings.set('check', today) settings.set('chain', chain) if version.has_608: ls = b'%416d' % today pa.ls_change(ls) ll, sha = await write_complete_backup(words, None, True) result = SFFile(0, ll).read() if words: #open('debug.7z', 'wb').write(result) assert ll > 800 assert len(sha) == 32 assert result[0:6] == b"7z\xbc\xaf'\x1c" assert ngu.hash.sha256s(result) == sha assert len(set(result)) >= 240 # encrypted else: sr = str(result, 'ascii') print("Backup contents:\n" + sr) assert sr[0] == '#', result assert 'Coldcard' in sr assert len(set(sr)) < 100 # cleartext, english assert ('chain = "%s"' % chain) in result # test restore # - cant wipe flash, since the backup file is there # - cant wipe all settings becuase PIN and stuff is simulated there del settings.current['check'] if had_policy: from hsm import POLICY_FNAME uos.unlink(POLICY_FNAME) assert not hsm.hsm_policy_available() with SFFile(0, ll) as fd: numpad.inject('y') # for 'success' message await restore_complete_doit(fd, words) assert settings.get('check') == today, \ (settings.get('check'), '!=', today) assert settings.get('chain') == chain, \ (settings.get('chain'), '!=', chain) if version.has_608: assert pa.ls_fetch() == ls if had_policy: assert had_policy == hsm.hsm_policy_available() today += 3 import ux ux.restore_menu()
async def interact(self): # Prompt user w/ details and get approval from common import dis # step 1: parse PSBT from sflash into in-memory objects. dis.fullscreen("Validating...") if self.psbt == None: try: # Read TXN from SPI Flash (we put it there whether it came from a QR code or an SD card) with SFFile(TXN_INPUT_OFFSET, length=self.psbt_len) as fd: self.psbt = psbtObject.read_psbt(fd) except BaseException as exc: if isinstance(exc, MemoryError): msg = "Transaction is too complex" exc = None else: msg = "PSBT parse failed" return await self.failure(msg, exc) # 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: del self.psbt gc.collect() if isinstance(exc, MemoryError): msg = "Transaction is too complex" exc = None else: msg = "Invalid PSBT" 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: outputs = uio.StringIO() outputs.write('Amount:') first = True for idx, tx_out in self.psbt.output_iter(): outp = self.psbt.outputs[idx] if outp.is_change: continue if first: first = False else: outputs.write('\n') outputs.write(self.render_output(tx_out)) # print('total_out={} total_in={} change={}'.format=(self.psbt.total_value_out, self.psbt.total_value_in, self.psbt.total_value_in - self.psbt.total_value_out)) pages = [ {'title': 'Sign Txn', 'msg': outputs.getvalue(), 'center': True, 'center_vertically': True}, {'title': 'Sign Txn', 'msg': self.render_change_text(), 'center': True, 'center_vertically': True}, ] warnings = self.render_warnings() print('warnings = "{}"'.format(warnings)) if warnings != None: pages.append( {'title': 'Sign Txn', 'msg': warnings, 'center': True, 'center_vertically': True, 'right_btn': 'SIGN!'} ) if self.do_visualize: # stop here and just return the text of approval message itself self.result = await self.save_visualization(msg, (self.stxn_flags & STXN_SIGNED)) del self.psbt self.done() return result = await ux_show_story_sequence(pages) except MemoryError: # 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) if result != 'y': # User chose not to sign the transaction self.refused = True # TODO: ux_confirm() instead? # 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: return await self.failure(exc.args[0], title='Change Fraud') except MemoryError: msg = "Transaction is too complex" return await self.failure(msg) except BaseException as exc: return await self.failure("Signing failed late", 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: return await self.failure("PSBT output failed", exc)