def encryption_key(self, salt): # Return a 32-byte derived secret to be used for our own internal encryption purposes # 0x80000000 - 0xCC30 = 2147431376 node = self.derive_path("m/2147431408'/0'") # plan: 0' will be an index for other apps acc = tcc.sha256(salt) acc.update(node.private_key()) acc.update(salt) pk = tcc.sha256(acc.digest()).digest() self.register(pk) return pk
def hash_message(cls, msg): # Perform sha256 for message-signing purposes (only) s = tcc.sha256() prefix = cls.msg_signing_prefix() assert len(prefix) < 253 s.update(bytes([len(prefix)])) s.update(prefix) assert len(msg) < 253 s.update(bytes([len(msg)])) s.update(msg) return tcc.sha256(s.digest()).digest()
def handle_crypto_setup(self, version, his_pubkey): # pick a one-time key pair for myself, and return the pubkey for that # determine what the session key will be for this connection assert version == 0x1 assert len(his_pubkey) == 64 # pick a random key pair, just for this session my_key = tcc.secp256k1.generate_secret() my_pubkey = tcc.secp256k1.publickey(my_key, False) #print('my pubkey = ' + str(b2a_hex(my_pubkey))) #print('his pubkey = ' + str(b2a_hex(his_pubkey))) pt = tcc.secp256k1.multiply(my_key, b'\x04' + his_pubkey) assert pt[0] == 4 self.session_key = tcc.sha256(pt[1:]).digest() #print("session = " + str(b2a_hex(self.session_key))) # Would be nice to have nonce in addition to the counter, but # library not ready for that, and also harder on the desktop side. self.encrypt = tcc.AES(tcc.AES.CTR | tcc.AES.Encrypt, self.session_key) self.decrypt = tcc.AES(tcc.AES.CTR | tcc.AES.Decrypt, self.session_key) from main import settings xfp = settings.get('xfp', 0) xpub = settings.get('xpub', '') assert my_pubkey[0] == 0x04 return b'mypb' + my_pubkey[1:] + pack('<II', xfp, len(xpub)) + xpub
async def handle_upload(self, offset, total_size, data): from main import dis, sf, hsm_active from utils import check_firmware_hdr from sigheader import FW_HEADER_OFFSET, FW_HEADER_SIZE # maintain a running SHA256 over what's received if offset == 0: self.file_checksum = tcc.sha256() assert offset % 256 == 0, 'alignment' assert offset + len(data) <= total_size <= MAX_UPLOAD_LEN, 'long' if hsm_active: # additional restrictions in HSM mode assert offset + len(data) <= total_size <= MAX_TXN_LEN, 'psbt' if offset == 0: assert data[0:5] == b'psbt\xff', 'psbt' for pos in range(offset, offset + len(data), 256): if pos % 4096 == 0: # erase here dis.fullscreen("Receiving...", offset / total_size) sf.sector_erase(pos) while sf.is_busy(): await sleep_ms(10) # write up to 256 bytes here = data[pos - offset:pos - offset + 256] self.file_checksum.update(here) # Very special case for firmware upgrades: intercept and modify # header contents on the fly, and also fail faster if wouldn't work # on this specific hardware. # - workaround: ckcc-protocol upgrade process understates the file # length and appends hdr, but that's kinda a bug, so support both if (pos == (FW_HEADER_OFFSET & ~255) or pos == (total_size - FW_HEADER_SIZE) or pos == total_size): prob = check_firmware_hdr(memoryview(here)[-128:], None, bad_magic_ok=True) if prob: raise ValueError(prob) sf.write(pos, here) # full page write: 0.6 to 3ms while sf.is_busy(): await sleep_ms(1) if offset + len(data) >= total_size and not hsm_active: # probably done dis.progress_bar_show(1.0) ux.restore_menu() return offset
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())
async def list_files(*A): # list files, don't do anything with them? fn = await file_picker( 'Lists all files on MicroSD. Select one and SHA256(file contents) will be shown.', min_size=0) if not fn: return import tcc from utils import B2A chk = tcc.sha256() try: with CardSlot() as card: with open(fn, 'rb') as fp: while 1: data = fp.read(1024) if not data: break chk.update(data) except CardMissingError: await needs_microsd() return basename = fn.rsplit('/', 1)[-1] ch = await ux_show_story('''SHA256(%s)\n\n%s\n\nPress 6 to delete.''' % (basename, B2A(chk.digest())), escape='6') if ch == '6': from files import securely_blank_file securely_blank_file(fn) return
async def approve_msg_sign(self, msg_text, address, subpath): # Maybe approve indicated message to be signed. # return 'y' or 'x' sha = tcc.sha256(msg_text).digest() with AuditLogger('messages', 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('Message signing requested:') log.info('SHA256(msg) = ' + b2a_hex(sha).decode('ascii')) log.info('\n%d bytes to be signed by %s => %s' % (len(msg_text), subpath, address)) if not self.msg_paths: self.refuse(log, "Message signing not permitted") return 'x' if not match_deriv_path(self.msg_paths, subpath): self.refuse(log, 'Message signing not enabled for that path') return 'x' self.approve(log, 'Message signing allowed') return 'y'
def p2sh_address(cls, addr_fmt, witdeem_script): # Multisig and general P2SH support # - witdeem => witness script for segwit, or redeem script otherwise # - redeem script can be generated from witness script if needed. # - this function needs a witdeem script to be provided, not simple to make # - more verification needed to prove it's change/included address (NOT HERE) # - reference: <https://bitcoincore.org/en/segwit_wallet_dev/> # - returns: str(address) assert addr_fmt & AFC_SCRIPT, 'for p2sh only' assert witdeem_script, "need witness/redeem script" if addr_fmt & AFC_SEGWIT: digest = tcc.sha256(witdeem_script).digest() else: digest = hash160(witdeem_script) if addr_fmt & AFC_BECH32: # bech32 encoded segwit p2sh addr = tcc.codecs.bech32_encode(cls.bech32_hrp, 0, digest) elif addr_fmt == AF_P2WSH_P2SH: # segwit p2wsh encoded as classic P2SH addr = tcc.codecs.b58_encode(cls.b58_script + hash160(b'\x00\x20' + digest)) else: # P2SH classic addr = tcc.codecs.b58_encode(cls.b58_script + digest) return addr
async def handle_download(self, offset, length, file_number): # let them read from where we store the signed txn # - filenumber can be 0 or 1: uploaded txn, or result from main import sf # limiting memory use here, should be MAX_BLK_LEN really length = min(length, MAX_BLK_LEN) assert 0 <= file_number < 2, 'bad fnum' assert 0 <= offset <= MAX_TXN_LEN, "bad offset" assert 1 <= length, 'len' # maintain a running SHA256 over what's sent if offset == 0: self.file_checksum = tcc.sha256() pos = (MAX_TXN_LEN * file_number) + offset resp = bytearray(4 + length) resp[0:4] = b'biny' sf.read(pos, memoryview(resp)[4:]) self.file_checksum.update(memoryview(resp)[4:]) return resp
def encode_key(cls, prevout): # hash up the txid and output number, truncate, and encode as base64 # - truncating at (mod3) bytes so no padding on b64 output # - expects a COutPoint md = tcc.sha256('OutptValueCache') md.update(prevout.serialize()) return b2a_base64(md.digest()[:15])[:-1].decode()
async def test_sflash(): dis.clear() dis.text(None, 18, 'Serial Flash') dis.show() #if ckcc.is_simulator(): return from main import sf from ustruct import pack import tcc msize = 1024 * 1024 sf.chip_erase() for phase in [0, 1]: steps = 7 * 4 for i in range(steps): dis.progress_bar(i / steps) dis.show() await sleep_ms(250) if not sf.is_busy(): break assert not sf.is_busy(), "sflash erase didn't finish" # leave chip blank if phase == 1: break buf = bytearray(32) for addr in range(0, msize, 1024): sf.read(addr, buf) assert set(buf) == {255}, "sflash not blank:" + repr(buf) rnd = tcc.sha256(pack('I', addr)).digest() sf.write(addr, rnd) sf.read(addr, buf) assert buf == rnd, "sflash write failed" dis.progress_bar_show(addr / msize) # check no aliasing, also right size part for addr in range(0, msize, 1024): expect = tcc.sha256(pack('I', addr)).digest() sf.read(addr, buf) assert buf == expect, "sflash readback failed" dis.progress_bar_show(addr / msize)
def hash_message(cls, msg=None, msg_len=0): # Perform sha256 for message-signing purposes (only) # - or get setup for that, if msg == None s = tcc.sha256() s.update(cls.msg_signing_prefix()) msg_len = msg_len or len(msg) s.update(ser_compact_size(msg_len)) if msg is None: return s s.update(msg) return tcc.sha256(s.digest()).digest()
async def add_dice_rolls(count, seed, judge_them): from main import dis from display import FontTiny, FontLarge md = tcc.sha256(seed) pr = PressRelease() # fixed parts of screen dis.clear() y = 38 dis.text(0, y, "Press 1-6 for each dice") y += 13 dis.text(0, y, "roll to mix in.") dis.save() while 1: # Note: cannot scroll this msg because 5=up arrow dis.restore() dis.text(None, 0, '%d rolls' % count, FontLarge) hx = str(b2a_hex(md.digest()), 'ascii') dis.text(0, 20, hx[0:32], FontTiny) dis.text(0, 20 + 7, hx[32:], FontTiny) dis.show() ch = await pr.wait() if ch in '123456': count += 1 dis.restore() dis.text(None, 0, '%d rolls' % count, FontLarge) dis.show() # this is slow enough to see md.update(ch) elif ch == 'x': # Because the change (roll) has already been applied, # only let them abort if it's early still if count < 10 and judge_them: return 0, seed elif ch == 'y': if count < 99 and judge_them: if not count: return 0, seed ok = await ux_confirm('''\ You only provided %d dice rolls, and each roll adds only 2.585 bits of entropy. \ For 128-bit security, which is considered the minimum, you need 50 rolls, and \ for 256-bits of security, 99 rolls.''' % count) if not ok: continue break if count: seed = md.digest() return count, seed
def get_hash256(self, val, hasher=None): # return the double-sha256 of a value, without loading it into memory pos, ll = val rv = hasher or tcc.sha256() self.fd.seek(pos) while ll: here = self.fd.read_into(psbt_tmp256) if not here: break if here > ll: here = ll rv.update(memoryview(psbt_tmp256)[0:here]) ll -= here if hasher: return return tcc.sha256(rv.digest()).digest()
def calc_hmac_key(text_password): # Calculate a 32-byte key based on user's text password, PBKDF2_ITER_COUNT, # and device serial number as salt. import version salt = tcc.sha256(b'pepper' + version.serial_number().encode()).digest() pw = tcc.pbkdf2('hmac-sha256', text_password, salt, PBKDF2_ITER_COUNT).key() return pw
def make_txn_sighash(self, replace_idx, replacement, sighash_type): # calculate the hash value for one input of current transaction # - blank all script inputs # - except one single tx in, which is provided # - serialize that without witness data # - append SIGHASH_ALL=1 value (LE32) # - sha256 over that fd = self.fd old_pos = fd.tell() rv = tcc.sha256() # version number rv.update(pack('<i', self.txn_version)) # nVersion # inputs rv.update(ser_compact_size(self.num_inputs)) for in_idx, txi in self.input_iter(): if in_idx == replace_idx: assert not self.inputs[in_idx].witness_utxo assert not self.inputs[in_idx].is_segwit assert replacement.scriptSig rv.update(replacement.serialize()) else: txi.scriptSig = b'' rv.update(txi.serialize()) # outputs rv.update(ser_compact_size(self.num_outputs)) for out_idx, txo in self.output_iter(): rv.update(txo.serialize()) # locktime rv.update(pack('<I', self.lock_time)) assert sighash_type == SIGHASH_ALL, "only SIGHASH_ALL supported" # SIGHASH_ALL==1 value rv.update(b'\x01\x00\x00\x00') fd.seek(old_pos) # double SHA256 return tcc.sha256(rv.digest()).digest()
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 calc_txid(self, poslen): # Given the (pos,len) of a transaction, return the txid for that. # - doesn't validate data # - does detected witness txn vs. old style # - simple dsha256() if old style txn, other wise witness must be skipped # see if witness encoding in effect fd = self.fd fd.seek(poslen[0]) txn_version, marker, flags = unpack("<iBB", fd.read(6)) has_witness = (marker == 0 and flags != 0x0) if not has_witness: # txn does not have witness data, so txid==wtxix return self.get_hash256(poslen) rv = tcc.sha256() # de/reserialize much of the txn -- but not the witness data rv.update(pack("<i", txn_version)) body_start = fd.tell() # determine how long ins + outs are... num_in = deser_compact_size(fd) _skip_n_objs(fd, num_in, 'CTxIn') num_out = deser_compact_size(fd) _skip_n_objs(fd, num_out, 'CTxOut') body_len = fd.tell() - body_start # hash the bulk of txn self.get_hash256((body_start, body_len), hasher=rv) # assume last 4 bytes are the lock_time fd.seek(sum(poslen) - 4) rv.update(fd.read(4)) return tcc.sha256(rv.digest()).digest()
def set_key(self, new_secret=None): # System settings (not secrets) are stored in SPI Flash, encrypted with this # key that is derived from main wallet secret. Call this method when the secret # is first loaded, or changes for some reason. from main import pa from stash import blank_object key = None mine = False if not new_secret: if not pa.is_successful() or pa.is_secret_blank(): # simple fixed key allows us to store a few things when logged out key = b'\0' * 32 else: # read secret and use it. new_secret = pa.fetch() mine = True if new_secret: # hash up the secret... without decoding it or similar assert len(new_secret) >= 32 s = tcc.sha256(new_secret) for round in range(5): s.update('pad') s = tcc.sha256(s.digest()) key = s.digest() if mine: blank_object(new_secret) # for restore from backup case, or when changing (created) the seed self.nvram_key = key
async def make_new_wallet(): # Pick a new random seed, and await ux_dramatic_pause('Generating...', 4) # always full 24-word (256 bit) entropy seed = bytearray(32) rng_bytes(seed) assert len(set(seed)) > 4 # TRNG failure # hash to mitigate bias in TRNG seed = tcc.sha256(seed).digest() await approve_word_list(seed)
def __init__(self, start, length=0, max_size=None, message=None, pre_erased=False): if not pre_erased: assert start % blksize == 0 # 'misaligned' self.start = start self.pos = 0 self.length = length # byte-wise length self.message = message if max_size != None: self.max_size = PADOUT(max_size) if not pre_erased else max_size self.readonly = False self.checksum = tcc.sha256() else: self.readonly = True from main import sf self.sf = sf
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) # 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.update(d)) chk.update(d) del d 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 calculate_key(self, password, progress_fcn=None): # do the expected key-derivation # emulate CKeyInfo::CalculateDigest in p7zip_9.38.1/CPP/7zip/Crypto/7zAes.cpp rounds = 1 << self.rounds_pow password = encode_utf_16_le(password) result = sha256() for i in range(rounds): result.update(self.salt) result.update(password) temp = pack('<Q', i) result.update(temp) if i % 1000 == 0 and progress_fcn: progress_fcn(i/rounds) return result.digest()
def __init__(self): self.dev = pyb.USB_HID() # We keep a running hash over whatever has been uploaded # - reset at offset zero, can be read back anytime self.file_checksum = tcc.sha256() # handle simulator self.blockable = getattr(self.dev, 'pipe', self.dev) #self.msg = bytearray(MAX_MSG_LEN) from sram2 import usb_buf self.msg = usb_buf assert len(self.msg) == MAX_MSG_LEN self.encrypted_req = False # these will be tcc.AES objects later self.encrypt = None self.decrypt = None
async def handle_upload(self, offset, total_size, data): from main import dis, sf # maintain a running SHA256 over what's received if offset == 0: self.file_checksum = tcc.sha256() assert offset % 256 == 0, 'alignment' assert offset + len(data) <= total_size <= MAX_UPLOAD_LEN, 'long' rb = bytearray(256) for pos in range(offset, offset + len(data), 256): if pos % 4096 == 0: # erase here sf.sector_erase(pos) dis.fullscreen("Receiving...") dis.progress_bar_show(offset / total_size) while sf.is_busy(): await sleep_ms(10) # write up to 256 bytes here = data[pos - offset:pos - offset + 256] sf.write(pos, here) # full page write: 0.6 to 3ms while sf.is_busy(): await sleep_ms(1) # use actual read back for verify sf.read(pos, rb) self.file_checksum.update(rb[0:len(here)]) if offset + len(data) >= total_size: # probably done dis.progress_bar_show(1.0) ux.restore_menu() return offset
def get_id_hash(self): # hash over card config and serial # details import tcc info = pyb.SDCard().info() assert info and len(info) >= 5 # need micropython changes return tcc.sha256(repr(info)).digest()
def load(self): # Search all slots for any we can read, decrypt that, # and pick the newest one (in unlikely case of dups) from main import sf # reset self.current = {} self.my_pos = 0 self.is_dirty = 0 # 4k, but last 32 bytes are a SHA (itself encrypted) global _tmp buf = bytearray(4) empty = 0 for pos in SLOTS: 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(tcc.AES.Encrypt, pos) chk = aes.update(b'{"') if chk != buf[0:2]: # doesn't look like JSON meant for me continue # probably good, read it aes = self.get_aes(tcc.AES.Encrypt, pos) chk = tcc.sha256() expect = None with SFFile(pos, length=4096, pre_erased=True) as fd: for i in range(4096 / 32): b = aes.update(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. d = ujson.load(BytesIO(_tmp)) 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) tcc.random.shuffle(blks) for pos in blks[0:3]: for i in range(0, 4096, 256): h = tcc.random.bytes(256) sf.wait_done() sf.write(pos + i, h)
def sha256(s): return tcc.sha256(s).digest()
async def make_new_wallet(): # pick a new random seed, and force them to # write it down, then save it. from main import dis from uasyncio import sleep_ms # CONCERN: memory is really contaminated with secrets in this process, much more so # than during normal operation. Maybe we should block USB and force a reboot as well? # LESSON LEARNED: if the user is writting down the words, as we have # vividly instructed, then it's a big deal to lose those words and have to start # over. So confirm that action, and don't volunteer it. # dramatic pause await ux_dramatic_pause('Generating...', 4) # always full 24-word (256 bit) entropy seed = bytearray(32) rng_bytes(seed) assert len(set(seed)) > 4, "impossible luck?" # hash to mitigate bias in TRNG seed = tcc.sha256(seed).digest() words = tcc.bip39.from_data(seed).split(' ') assert len(words) == 24 #print('words: ' + ' '.join(words)) while 1: # show the seed words ch = await show_words(words, escape='6') if ch == 'x': # user abort if await ux_confirm("Throw away those words and stop this process?" ): return else: continue if ch == '6': # wants to skip the quiz (undocumented) if await ux_confirm( "Skipping the quiz means you might have " "recorded the seed wrong and will be crying later."): break # Perform a test, to check they wrote them down ch = await word_quiz(words) if ch == 'x': # user abort quiz if await ux_confirm( "Throw away those words and stop this process? Press X to see the word list again and restart the quiz." ): return # show the words again, but don't change them continue # quiz passed break # Done! set_seed_value(words) # send them to home menu, now with a wallet enabled goto_top_menu()
def __init__(self, d=None): self.rv = tcc.sha256() print('Hashing: ', end='') if d: self.update(d)