From 0bcea80bdff10c89147ff5bb1db61ea01836d1dd Mon Sep 17 00:00:00 2001 From: "Peter D. Gray" Date: Tue, 26 Jun 2018 22:33:04 +0200 Subject: [PATCH] Support for new hardware wallet: Coldcard build-wine/deterministic.spec: add Coldcard plugin and ckcc-protocol dependancy Require version 0.7.2 of ckcc-protocol (window fixes) Rework import paths to new standards Updated icons New minimum version, for latest PSBT constants Upgrade to final PSBT (BIP 174) standard encoding Remove log noise Show bootloader version number as well Handle case where libraries are missing better Remove noise about missing packages, for rest of world Add reference to ckcc-protocol module/data Remove dead code Beef up the README more Slightly better looking Add version numbers and upgrade firmware feature Split out DFU support into own file First pass at adding Coinkite Coldcard hardware wallet to Electrum --- contrib/build-osx/osx.spec | 3 + contrib/build-wine/deterministic.spec | 3 + .../deterministic-build/requirements-hw.txt | 5 + contrib/requirements/requirements-hw.txt | 1 + electrum/plugins/coldcard/README.md | 65 ++ electrum/plugins/coldcard/__init__.py | 7 + electrum/plugins/coldcard/cmdline.py | 47 ++ electrum/plugins/coldcard/coldcard.py | 668 ++++++++++++++++++ electrum/plugins/coldcard/qt.py | 242 +++++++ electrum/transaction.py | 4 +- icons.qrc | 2 + icons/coldcard.png | Bin 0 -> 528 bytes icons/coldcard_unpaired.png | Bin 0 -> 788 bytes 13 files changed, 1045 insertions(+), 2 deletions(-) create mode 100644 electrum/plugins/coldcard/README.md create mode 100644 electrum/plugins/coldcard/__init__.py create mode 100644 electrum/plugins/coldcard/cmdline.py create mode 100644 electrum/plugins/coldcard/coldcard.py create mode 100644 electrum/plugins/coldcard/qt.py create mode 100644 icons/coldcard.png create mode 100644 icons/coldcard_unpaired.png diff --git a/contrib/build-osx/osx.spec b/contrib/build-osx/osx.spec index 0cd01c66d..2de6bfaf3 100644 --- a/contrib/build-osx/osx.spec +++ b/contrib/build-osx/osx.spec @@ -27,6 +27,7 @@ hiddenimports += collect_submodules('safetlib') hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('keepkeylib') hiddenimports += collect_submodules('websocket') +hiddenimports += collect_submodules('ckcc') datas = [ (electrum+'electrum/*.json', PYPKG), @@ -37,6 +38,7 @@ datas += collect_data_files('trezorlib') datas += collect_data_files('safetlib') datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') +datas += collect_data_files('ckcc') # Add libusb so Trezor and Safe-T mini will work binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")] @@ -63,6 +65,7 @@ a = Analysis([electrum+ MAIN_SCRIPT, electrum+'electrum/plugins/safe_t/qt.py', electrum+'electrum/plugins/keepkey/qt.py', electrum+'electrum/plugins/ledger/qt.py', + electrum+'electrum/plugins/coldcard/qt.py', ], binaries=binaries, datas=datas, diff --git a/contrib/build-wine/deterministic.spec b/contrib/build-wine/deterministic.spec index ae3fba808..63d75c2cf 100644 --- a/contrib/build-wine/deterministic.spec +++ b/contrib/build-wine/deterministic.spec @@ -22,6 +22,7 @@ hiddenimports += collect_submodules('safetlib') hiddenimports += collect_submodules('btchip') hiddenimports += collect_submodules('keepkeylib') hiddenimports += collect_submodules('websocket') +hiddenimports += collect_submodules('ckcc') # Add libusb binary binaries = [(PYHOME+"/libusb-1.0.dll", ".")] @@ -41,6 +42,7 @@ datas += collect_data_files('trezorlib') datas += collect_data_files('safetlib') datas += collect_data_files('btchip') datas += collect_data_files('keepkeylib') +datas += collect_data_files('ckcc') # We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports a = Analysis([home+'run_electrum', @@ -60,6 +62,7 @@ a = Analysis([home+'run_electrum', home+'electrum/plugins/safe_t/qt.py', home+'electrum/plugins/keepkey/qt.py', home+'electrum/plugins/ledger/qt.py', + home+'electrum/plugins/coldcard/qt.py', #home+'packages/requests/utils.py' ], binaries=binaries, diff --git a/contrib/deterministic-build/requirements-hw.txt b/contrib/deterministic-build/requirements-hw.txt index f06b43e10..ea2d91190 100644 --- a/contrib/deterministic-build/requirements-hw.txt +++ b/contrib/deterministic-build/requirements-hw.txt @@ -115,3 +115,8 @@ websocket-client==0.48.0 \ wheel==0.31.1 \ --hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \ --hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f +pyaes==1.6.1 \ + --hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f +ckcc-protocol==0.7.2 \ + --hash=sha256:498db4ccdda018cd9f40210f5bd02ddcc98e7df583170b2eab4035c86c3cc03b \ + --hash=sha256:31ee5178cfba8895eb2a6b8d06dc7830b51461a0ff767a670a64707c63e6b264 diff --git a/contrib/requirements/requirements-hw.txt b/contrib/requirements/requirements-hw.txt index 4fe6477f1..a6ae0a3a5 100644 --- a/contrib/requirements/requirements-hw.txt +++ b/contrib/requirements/requirements-hw.txt @@ -3,5 +3,6 @@ trezor[hidapi]>=0.9.0 safet[hidapi]>=0.1.0 keepkey btchip-python +ckcc-protocol>=0.7.2 websocket-client hidapi diff --git a/electrum/plugins/coldcard/README.md b/electrum/plugins/coldcard/README.md new file mode 100644 index 000000000..651cd8047 --- /dev/null +++ b/electrum/plugins/coldcard/README.md @@ -0,0 +1,65 @@ + +# Coldcard Hardware Wallet Plugin + +## Just the glue please + +This code connects the public USB API and Electrum. Leverages all +the good work that's been done by the Electrum team to support +hardware wallets. + +## Background + +The Coldcard has a larger screen (128x64) and a number pad. For +this reason, all PIN code entry is done directly on the device. +Coldcard does not appear on the USB bus until unlocked with appropriate +PIN. Initial setup, and seed generation must be done offline. + +Coldcard uses an emerging standard for unsigned tranasctions: + +PSBT = Partially Signed Bitcoin Transaction = BIP174 + +However, this spec is still under heavy discussion and in flux. At +this point, the PSBT files generated will only be compatible with +Coldcard. + +The Coldcard can be used 100% offline: it can generate a skeleton +Electrum wallet and save it to MicroSD card. Transport that file +to Electrum and it will fetch history, blockchain details and then +operate in "unpaired" mode. + +Spending transactions can be saved to MicroSD using the "Export PSBT" +button on the transaction preview dialog (when this plugin is +owner of the wallet). That PSBT can be signed on the Coldcard +(again using MicroSD both ways). The result is a ready-to-transmit +bitcoin transaction, which can be transmitted using Tools > Load +Transaction > From File in Electrum or really any tool. + + + +## TODO Items + +- No effort yet to support translations or languages other than English, sorry. +- Coldcard PSBT format is not likely to be compatible with other devices, because the BIP174 is still in flux. +- Segwit support not 100% complete: can pay to them, but cannot setup wallet to receive them. +- Limited support for segwit wrapped in P2SH. +- Someday we could support multisig hardware wallets based on PSBT where each participant + is using different devices/systems for signing, however, that belongs in an independant + plugin that is PSBT focused and might not require a Coldcard to be present. + +### Ctags + +- I find this command useful (at top level) ... but I'm a VIM user. + + ctags -f .tags electrum `find . -name ENV -prune -o -name \*.py` + + +### Working with latest ckcc-protocol + +- at top level, do this: + + pip install -e git+ssh://git@github.com/Coldcard/ckcc-protocol.git#egg=ckcc-protocol + +- but you'll need the https version of that, not ssh like I can. +- also a branch name would be good in there +- do `pip uninstall ckcc` first +- see diff --git a/electrum/plugins/coldcard/__init__.py b/electrum/plugins/coldcard/__init__.py new file mode 100644 index 000000000..7cb033f42 --- /dev/null +++ b/electrum/plugins/coldcard/__init__.py @@ -0,0 +1,7 @@ +from electrum.i18n import _ + +fullname = 'Coldcard Wallet' +description = 'Provides support for the Coldcard hardware wallet from Coinkite' +requires = [('ckcc-protocol', 'github.com/Coldcard/ckcc-protocol')] +registers_keystore = ('hardware', 'coldcard', _("Coldcard Wallet")) +available_for = ['qt', 'cmdline'] diff --git a/electrum/plugins/coldcard/cmdline.py b/electrum/plugins/coldcard/cmdline.py new file mode 100644 index 000000000..5fddd02a0 --- /dev/null +++ b/electrum/plugins/coldcard/cmdline.py @@ -0,0 +1,47 @@ +from electrum.plugin import hook +from .coldcard import ColdcardPlugin +from electrum.util import print_msg, print_error, raw_input, print_stderr + +class ColdcardCmdLineHandler: + + def get_passphrase(self, msg, confirm): + raise NotImplementedError + + def get_pin(self, msg): + raise NotImplementedError + + def prompt_auth(self, msg): + raise NotImplementedError + + def yes_no_question(self, msg): + print_msg(msg) + return raw_input() in 'yY' + + def stop(self): + pass + + def show_message(self, msg, on_cancel=None): + print_stderr(msg) + + def show_error(self, msg, blocking=False): + print_error(msg) + + def update_status(self, b): + print_error('hw device status', b) + + def finished(self): + pass + +class Plugin(ColdcardPlugin): + handler = ColdcardCmdLineHandler() + + @hook + def init_keystore(self, keystore): + if not isinstance(keystore, self.keystore_class): + return + keystore.handler = self.handler + + def create_handler(self, window): + return self.handler + +# EOF diff --git a/electrum/plugins/coldcard/coldcard.py b/electrum/plugins/coldcard/coldcard.py new file mode 100644 index 000000000..fd307de2a --- /dev/null +++ b/electrum/plugins/coldcard/coldcard.py @@ -0,0 +1,668 @@ +# +# Coldcard Electrum plugin main code. +# +# +from struct import pack, unpack +import hashlib +import os, sys, time, io +import traceback + +from electrum import bitcoin +from electrum.bitcoin import TYPE_ADDRESS, int_to_hex +from electrum.i18n import _ +from electrum.plugin import BasePlugin, Device +from electrum.keystore import Hardware_KeyStore, xpubkey_to_pubkey +from electrum.transaction import Transaction +from electrum.wallet import Standard_Wallet +from electrum.crypto import hash_160 +from ..hw_wallet import HW_PluginBase +from ..hw_wallet.plugin import is_any_tx_output_on_change_branch +from electrum.util import print_error, bfh, bh2u, versiontuple +from electrum.base_wizard import ScriptTypeNotSupported + +try: + import hid + from ckcc.protocol import CCProtocolPacker, CCProtocolUnpacker + from ckcc.protocol import CCProtoError, CCUserRefused, CCBusyError + from ckcc.constants import (MAX_MSG_LEN, MAX_BLK_LEN, MSG_SIGNING_MAX_LENGTH, MAX_TXN_LEN, + AF_CLASSIC, AF_P2SH, AF_P2WPKH, AF_P2WSH, AF_P2WPKH_P2SH, AF_P2WSH_P2SH) + from ckcc.constants import ( + PSBT_GLOBAL_UNSIGNED_TX, PSBT_IN_NON_WITNESS_UTXO, PSBT_IN_WITNESS_UTXO, + PSBT_IN_SIGHASH_TYPE, PSBT_IN_REDEEM_SCRIPT, PSBT_IN_WITNESS_SCRIPT, + PSBT_IN_BIP32_DERIVATION, PSBT_OUT_BIP32_DERIVATION) + + from ckcc.client import ColdcardDevice, COINKITE_VID, CKCC_PID, CKCC_SIMULATOR_PATH + + requirements_ok = True + + + class ElectrumColdcardDevice(ColdcardDevice): + # avoid use of pycoin for MiTM message signature test + def mitm_verify(self, sig, expect_xpub): + # verify a signature (65 bytes) over the session key, using the master bip32 node + # - customized to use specific EC library of Electrum. + from electrum.ecc import ECPubkey + + xtype, depth, parent_fingerprint, child_number, chain_code, K_or_k \ + = bitcoin.deserialize_xpub(expect_xpub) + + pubkey = ECPubkey(K_or_k) + try: + pubkey.verify_message_hash(sig[1:65], self.session_key) + return True + except: + return False + +except ImportError: + requirements_ok = False + + COINKITE_VID = 0xd13e + CKCC_PID = 0xcc10 + +CKCC_SIMULATED_PID = CKCC_PID ^ 0x55aa + +def my_var_int(l): + # Bitcoin serialization of integers... directly into binary! + if l < 253: + return pack("B", l) + elif l < 0x10000: + return pack("' % (self.dev.master_fingerprint, + self.label()) + + def verify_connection(self, expected_xfp, expected_xpub): + ex = (expected_xfp, expected_xpub) + + if self._expected_device == ex: + # all is as expected + return + + if ( (self._expected_device is not None) + or (self.dev.master_fingerprint != expected_xfp) + or (self.dev.master_xpub != expected_xpub)): + # probably indicating programing error, not hacking + raise RuntimeError("Expecting 0x%08x but that's not whats connected?!" % + expected_xfp) + + # check signature over session key + # - mitm might have lied about xfp and xpub up to here + # - important that we use value capture at wallet creation time, not some value + # we read over USB today + self.dev.check_mitm(expected_xpub=expected_xpub) + + self._expected_device = ex + + print_error("[coldcard]", "Successfully verified against MiTM") + + def is_pairable(self): + # can't do anything w/ devices that aren't setup (but not normally reachable) + return bool(self.dev.master_xpub) + + def timeout(self, cutoff): + # nothing to do? + pass + + def close(self): + # close the HID device (so can be reused) + self.dev.close() + self.dev = None + + def is_initialized(self): + return bool(self.dev.master_xpub) + + def label(self): + # 'label' of this Coldcard. Warning: gets saved into wallet file, which might + # not be encrypted, so better for privacy if based on xpub/fingerprint rather than + # USB serial number. + if self.dev.is_simulator: + lab = 'Coldcard Simulator 0x%08x' % self.dev.master_fingerprint + elif not self.dev.master_fingerprint: + # failback; not expected + lab = 'Coldcard #' + self.dev.serial + else: + lab = 'Coldcard 0x%08x' % self.dev.master_fingerprint + + # Hack zone: during initial setup I need the xfp and master xpub but + # very few objects are passed between the various steps of base_wizard. + # Solution: return a string with some hidden metadata + # - see + # - needs to work w/ deepcopy + class LabelStr(str): + def __new__(cls, s, xfp=None, xpub=None): + self = super().__new__(cls, str(s)) + self.xfp = getattr(s, 'xfp', xfp) + self.xpub = getattr(s, 'xpub', xpub) + return self + + return LabelStr(lab, self.dev.master_fingerprint, self.dev.master_xpub) + + def has_usable_connection_with_device(self): + # Do end-to-end ping test + try: + self.ping_check() + return True + except: + return False + + def get_xpub(self, bip32_path, xtype): + # TODO: xtype? .. might not be able to support anything but classic p2pkh? + print_error('[coldcard]', 'Derive xtype = %r' % xtype) + return self.dev.send_recv(CCProtocolPacker.get_xpub(bip32_path), timeout=5000) + + def ping_check(self): + # check connection is working + assert self.dev.session_key, 'not encrypted?' + req = b'1234 Electrum Plugin 4321' # free up to 59 bytes + try: + echo = self.dev.send_recv(CCProtocolPacker.ping(req)) + assert echo == req + except: + raise RuntimeError("Communication trouble with Coldcard") + + def show_address(self, path, addr_fmt): + # prompt user w/ addres, also returns it immediately. + return self.dev.send_recv(CCProtocolPacker.show_address(path, addr_fmt), timeout=None) + + def get_version(self): + # gives list of strings + return self.dev.send_recv(CCProtocolPacker.version(), timeout=1000).split('\n') + + def sign_message_start(self, path, msg): + # this starts the UX experience. + self.dev.send_recv(CCProtocolPacker.sign_message(msg, path), timeout=None) + + def sign_message_poll(self): + # poll device... if user has approved, will get tuple: (addr, sig) else None + return self.dev.send_recv(CCProtocolPacker.get_signed_msg(), timeout=None) + + def sign_transaction_start(self, raw_psbt, finalize=True): + # Multiple steps to sign: + # - upload binary + # - start signing UX + # - wait for coldcard to complete process, or have it refused. + # - download resulting txn + assert 20 <= len(raw_psbt) < MAX_TXN_LEN, 'PSBT is too big' + dlen, chk = self.dev.upload_file(raw_psbt) + + resp = self.dev.send_recv(CCProtocolPacker.sign_transaction(dlen, chk, finalize=finalize), + timeout=None) + + if resp != None: + raise ValueError(resp) + + def sign_transaction_poll(self): + # poll device... if user has approved, will get tuple: (legnth, checksum) else None + return self.dev.send_recv(CCProtocolPacker.get_signed_txn(), timeout=None) + + def download_file(self, length, checksum, file_number=1): + # get a file + return self.dev.download_file(length, checksum, file_number=file_number) + + + +class Coldcard_KeyStore(Hardware_KeyStore): + hw_type = 'coldcard' + device = 'Coldcard' + + def __init__(self, d): + Hardware_KeyStore.__init__(self, d) + # Errors and other user interaction is done through the wallet's + # handler. The handler is per-window and preserved across + # device reconnects + self.force_watching_only = False + self.ux_busy = False + + # Seems like only the derivation path and resulting **derived** xpub is stored in + # the wallet file... however, we need to know at least the fingerprint of the master + # xpub to verify against MiTM, and also so we can put the right value into the subkey paths + # of PSBT files that might be generated offline. + # - save the fingerprint of the master xpub, as "xfp" + # - it's a LE32 int, but hex more natural way to see it + # - device reports these value during encryption setup process + lab = d['label'] + if hasattr(lab, 'xfp'): + # initial setup + self.ckcc_xfp = lab.xfp + self.ckcc_xpub = lab.xpub + else: + # wallet load: fatal if missing, we need them! + self.ckcc_xfp = d['ckcc_xfp'] + self.ckcc_xpub = d['ckcc_xpub'] + + def dump(self): + # our additions to the stored data about keystore -- only during creation? + d = Hardware_KeyStore.dump(self) + + d['ckcc_xfp'] = self.ckcc_xfp + d['ckcc_xpub'] = self.ckcc_xpub + + return d + + def get_derivation(self): + return self.derivation + + def get_client(self): + # called when user tries to do something like view address, sign somthing. + # - not called during probing/setup + rv = self.plugin.get_client(self) + if rv: + rv.verify_connection(self.ckcc_xfp, self.ckcc_xpub) + + return rv + + def give_error(self, message, clear_client=False): + print_error(message) + if not self.ux_busy: + self.handler.show_error(message) + else: + self.ux_busy = False + if clear_client: + self.client = None + raise Exception(message) + + def wrap_busy(func): + # decorator: function takes over the UX on the device. + def wrapper(self, *args, **kwargs): + try: + self.ux_busy = True + return func(self, *args, **kwargs) + finally: + self.ux_busy = False + return wrapper + + def decrypt_message(self, pubkey, message, password): + raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device)) + + @wrap_busy + def sign_message(self, sequence, message, password): + # Sign a message on device. Since we have big screen, of course we + # have to show the message unabiguously there first! + try: + msg = message.encode('ascii', errors='strict') + assert 1 <= len(msg) <= MSG_SIGNING_MAX_LENGTH + except (UnicodeError, AssertionError): + # there are other restrictions on message content, + # but let the device enforce and report those + self.handler.show_error('Only short (%d max) ASCII messages can be signed.' + % MSG_SIGNING_MAX_LENGTH) + return b'' + + client = self.get_client() + path = self.get_derivation() + ("/%d/%d" % sequence) + try: + cl = self.get_client() + try: + self.handler.show_message("Signing message (using %s)..." % path) + + cl.sign_message_start(path, msg) + + while 1: + # How to kill some time, without locking UI? + time.sleep(0.250) + + resp = cl.sign_message_poll() + if resp is not None: + break + + finally: + self.handler.finished() + + assert len(resp) == 2 + addr, raw_sig = resp + + # already encoded in Bitcoin fashion, binary. + assert 40 < len(raw_sig) <= 65 + + return raw_sig + + except (CCUserRefused, CCBusyError) as exc: + self.handler.show_error(str(exc)) + except CCProtoError as exc: + traceback.print_exc(file=sys.stderr) + self.handler.show_error('{}\n\n{}'.format( + _('Error showing address') + ':', str(exc))) + except Exception as e: + self.give_error(e, True) + + # give empty bytes for error cases; it seems to clear the old signature box + return b'' + + def build_psbt(self, tx, wallet=None, xfp=None): + # Render a PSBT file, for upload to Coldcard. + # + if xfp is None: + # need fingerprint of MASTER xpub, not the derived key + xfp = self.ckcc_xfp + + inputs = tx.inputs() + + if 'prev_tx' not in inputs[0]: + # fetch info about inputs, if needed? + # - needed during export PSBT flow, not normal online signing + assert wallet, 'need wallet reference' + wallet.add_hw_info(tx) + + # wallet.add_hw_info installs this attr + assert hasattr(tx, 'output_info'), 'need data about outputs' + + # Build map of pubkey needed as derivation from master, in PSBT binary format + # 1) binary version of the common subpath for all keys + # m/ => fingerprint LE32 + # a/b/c => ints + base_path = pack(' not multisig, must be bip32 + if type(wallet) is not Standard_Wallet: + keystore.handler.show_error(_('This function is only available for standard wallets when using {}.').format(self.device)) + return + + sequence = wallet.get_address_index(address) + txin_type = wallet.get_txin_type(address) + keystore.show_address(sequence, txin_type) + +# EOF diff --git a/electrum/plugins/coldcard/qt.py b/electrum/plugins/coldcard/qt.py new file mode 100644 index 000000000..7e9c897f4 --- /dev/null +++ b/electrum/plugins/coldcard/qt.py @@ -0,0 +1,242 @@ +import time + +from electrum.i18n import _ +from electrum.plugin import hook +from electrum.wallet import Standard_Wallet +from electrum.gui.qt.util import * + +from .coldcard import ColdcardPlugin +from ..hw_wallet.qt import QtHandlerBase, QtPluginBase + + +class Plugin(ColdcardPlugin, QtPluginBase): + icon_unpaired = ":icons/coldcard_unpaired.png" + icon_paired = ":icons/coldcard.png" + + def create_handler(self, window): + return Coldcard_Handler(window) + + @hook + def receive_menu(self, menu, addrs, wallet): + if type(wallet) is not Standard_Wallet: + return + keystore = wallet.get_keystore() + if type(keystore) == self.keystore_class and len(addrs) == 1: + def show_address(): + keystore.thread.add(partial(self.show_address, wallet, addrs[0])) + menu.addAction(_("Show on Coldcard"), show_address) + + @hook + def transaction_dialog(self, dia): + # see gui/qt/transaction_dialog.py + + keystore = dia.wallet.get_keystore() + if type(keystore) != self.keystore_class: + # not a Coldcard wallet, hide feature + return + + # - add a new button, near "export" + btn = QPushButton(_("Save PSBT")) + btn.clicked.connect(lambda unused: self.export_psbt(dia)) + if dia.tx.is_complete(): + # but disable it for signed transactions (nothing to do if already signed) + btn.setDisabled(True) + + dia.sharing_buttons.append(btn) + + def export_psbt(self, dia): + # Called from hook in transaction dialog + tx = dia.tx + + if tx.is_complete(): + # if they sign while dialog is open, it can transition from unsigned to signed, + # which we don't support here, so do nothing + return + + # can only expect Coldcard wallets to work with these files (right now) + keystore = dia.wallet.get_keystore() + assert type(keystore) == self.keystore_class + + # convert to PSBT + raw_psbt = keystore.build_psbt(tx, wallet=dia.wallet) + + name = (dia.wallet.basename() + time.strftime('-%y%m%d-%H%M.psbt')).replace(' ', '-') + fileName = dia.main_window.getSaveFileName(_("Select where to save the PSBT file"), + name, "*.psbt") + if fileName: + with open(fileName, "wb+") as f: + f.write(raw_psbt) + dia.show_message(_("Transaction exported successfully")) + dia.saved = True + + def show_settings_dialog(self, window, keystore): + # When they click on the icon for CC we come here. + device_id = self.choose_device(window, keystore) + if device_id: + CKCCSettingsDialog(window, self, keystore, device_id).exec_() + + +class Coldcard_Handler(QtHandlerBase): + setup_signal = pyqtSignal() + #auth_signal = pyqtSignal(object) + + def __init__(self, win): + super(Coldcard_Handler, self).__init__(win, 'Coldcard') + self.setup_signal.connect(self.setup_dialog) + #self.auth_signal.connect(self.auth_dialog) + + + def message_dialog(self, msg): + self.clear_dialog() + self.dialog = dialog = WindowModalDialog(self.top_level_window(), _("Coldcard Status")) + l = QLabel(msg) + vbox = QVBoxLayout(dialog) + vbox.addWidget(l) + dialog.show() + + def get_setup(self): + self.done.clear() + self.setup_signal.emit() + self.done.wait() + return + + def setup_dialog(self): + self.show_error(_('Please initialization your Coldcard while disconnected.')) + return + +class CKCCSettingsDialog(WindowModalDialog): + '''This dialog doesn't require a device be paired with a wallet. + We want users to be able to wipe a device even if they've forgotten + their PIN.''' + + def __init__(self, window, plugin, keystore, device_id): + title = _("{} Settings").format(plugin.device) + super(CKCCSettingsDialog, self).__init__(window, title) + self.setMaximumWidth(540) + + devmgr = plugin.device_manager() + config = devmgr.config + handler = keystore.handler + self.thread = thread = keystore.thread + + def connect_and_doit(): + client = devmgr.client_by_id(device_id) + if not client: + raise RuntimeError("Device not connected") + return client + + body = QWidget() + body_layout = QVBoxLayout(body) + grid = QGridLayout() + grid.setColumnStretch(2, 1) + + # see + title = QLabel('''
+Coldcard Wallet +
from Coinkite Inc. +
coldcardwallet.com''') + title.setTextInteractionFlags(Qt.LinksAccessibleByMouse) + + grid.addWidget(title , 0,0, 1,2, Qt.AlignHCenter) + y = 3 + + rows = [ + ('fw_version', _("Firmware Version")), + ('fw_built', _("Build Date")), + ('bl_version', _("Bootloader")), + ('xfp', _("Master Fingerprint")), + ('serial', _("USB Serial")), + ] + for row_num, (member_name, label) in enumerate(rows): + widget = QLabel('000000000000') + widget.setTextInteractionFlags(Qt.TextSelectableByMouse | Qt.TextSelectableByKeyboard) + + grid.addWidget(QLabel(label), y, 0, 1,1, Qt.AlignRight) + grid.addWidget(widget, y, 1, 1, 1, Qt.AlignLeft) + setattr(self, member_name, widget) + y += 1 + body_layout.addLayout(grid) + + upg_btn = QPushButton('Upgrade') + #upg_btn.setDefault(False) + def _start_upgrade(): + thread.add(connect_and_doit, on_success=self.start_upgrade) + upg_btn.clicked.connect(_start_upgrade) + + y += 3 + grid.addWidget(upg_btn, y, 0) + grid.addWidget(CloseButton(self), y, 1) + + dialog_vbox = QVBoxLayout(self) + dialog_vbox.addWidget(body) + + # Fetch values and show them + thread.add(connect_and_doit, on_success=self.show_values) + + def show_values(self, client): + dev = client.dev + + self.xfp.setText('0x%08x' % dev.master_fingerprint) + self.serial.setText('%s' % dev.serial) + + # ask device for versions: allow extras for future + fw_date, fw_rel, bl_rel, *rfu = client.get_version() + + self.fw_version.setText('%s' % fw_rel) + self.fw_built.setText('%s' % fw_date) + self.bl_version.setText('%s' % bl_rel) + + def start_upgrade(self, client): + # ask for a filename (must have already downloaded it) + mw = get_parent_main_window(self) + dev = client.dev + + fileName = mw.getOpenFileName("Select upgraded firmware file", "*.dfu") + if not fileName: + return + + from ckcc.utils import dfu_parse + from ckcc.sigheader import FW_HEADER_SIZE, FW_HEADER_OFFSET, FW_HEADER_MAGIC + from ckcc.protocol import CCProtocolPacker + from hashlib import sha256 + import struct + + try: + with open(fileName, 'rb') as fd: + + # unwrap firmware from the DFU + offset, size, *ignored = dfu_parse(fd) + + fd.seek(offset) + firmware = fd.read(size) + + hpos = FW_HEADER_OFFSET + hdr = bytes(firmware[hpos:hpos + FW_HEADER_SIZE]) # needed later too + magic = struct.unpack_from("icons/speaker.png icons/trezor_unpaired.png icons/trezor.png + icons/coldcard.png + icons/coldcard_unpaired.png icons/trustedcoin-status.png icons/trustedcoin-wizard.png icons/unconfirmed.png diff --git a/icons/coldcard.png b/icons/coldcard.png new file mode 100644 index 0000000000000000000000000000000000000000..74104d76ec3745dac1461c5ad439c5bf32e249b6 GIT binary patch literal 528 zcmeAS@N?(olHy`uVBq!ia0vp^0U*r51|<6gKdl8)k|nMYCBgY=CFO}lsSJ)O`AMk? zp1FzXsX?iUDV2pMQ*9U+7&m#kIEGZ*dNap2uh~Grt?>s#?vk8My&+5F7Iw=t9xiJ1 zht9Gp`_KeYE z%y@X$PKtHU?TRA;-B%vk_PJgP-Kb8o9oGt2jDCf(W_Jw)$qzN0!dVhLBiZpYCm z`997?uaug88Ql^t_PM$&u5sVt%O@^osO`3WzqjGr*N-#z&Jv$mzWQwA?#uUPudmzw?Cer&$e=AH>YV~lthEa-qy8k=>@@kQ(}%* z@D{xJaOvN_oQ8Et84qtfKcMOU{BU~A(SXJ^hZtOLF)UwG1L3aBS@PE2KE3(1mYRmT zl4Qqfi6{yE9`R$`%6kmrW^l169O8c5deh@^>&HV|4B|jiZWW1p48r&tIP}kGffPP& z%>)X7q&^%9Q2WP)zp{4Y-2{Cgc!sp-8@)*WB_Xe@^`3&pG)E@IOkbVXNFu;4fN`VK4 zxa=KX0??r3XX z`gmxec~A|o+%6QDs;p66PCP(FVCm1R2!MpZilLDJ^ef-1n3k@Np-&8r8!+|?48b2M zj;+1NOHvEhB)9;0&2fyq0y}Ripu6SNF3cSm8o+0DCHFX%*uCbnY%gG=^RbT542c4{ zrxz8#QfO5ZD>@_Xe*dUrUC9a9GiU})ffL2C8elk!NTKpL$FVA4!7EGVZ96G!>NrN3 z#)x!Fueexkm&@hnwd9|PmI`obTa$(XENfo_1x&;ox8Fet!vfLoHq zv6*UNvh+d{z&jN%$g^aQ08!lXS#Ab-mdpttid&{7W(fS;4lG$Nz*1n&OJHYnVN|!A*-+0129<@D+SYU7SJtBst5rBwSejx;R2&H4 zRt^^4iy`IIQIp_O^%<_f8g)S?Le+;P2KxQ6ut%mgU^9X18EN(axt@`pCXLyHi8~_= z9g8YZVkM#g)y4s&s%uBr!m2>dd7#$9gzhZ57Veq*jkexnwA=@%X^aB=yB0V;?j`9$ ziFhuxS0UG2gDeta3%qI