Browse Source

wizard: add Digital Bitbox initialization to new wizard, remove rescan button

Note: the option to load a backup from SD card when the device already has a seed
has been removed. The device always returns an error when attempting this.
master
Sander van Grieken 2 years ago
parent
commit
b7612605c5
  1. 31
      electrum/gui/qt/wizard/wallet.py
  2. 8
      electrum/gui/qt/wizard/wizard.py
  3. 53
      electrum/plugins/digitalbitbox/digitalbitbox.py
  4. 66
      electrum/plugins/digitalbitbox/qt.py
  5. 4
      electrum/plugins/hw_wallet/plugin.py

31
electrum/gui/qt/wizard/wallet.py

@ -27,7 +27,8 @@ from electrum.gui.qt.bip39_recovery_dialog import Bip39RecoveryDialog
from electrum.gui.qt.password_dialog import PasswordLayout, PW_NEW, MSG_ENTER_PASSWORD, PasswordLayoutForHW
from electrum.gui.qt.seed_dialog import SeedLayout, MSG_PASSPHRASE_WARN_ISSUE4566, KeysLayout
from electrum.gui.qt.util import (PasswordLineEdit, char_width_in_lineedit, WWLabel, InfoButton, font_height,
ChoiceWidget, MessageBoxMixin)
ChoiceWidget, MessageBoxMixin, WindowModalDialog, ChoicesLayout, CancelButton,
Buttons, OkButton)
if TYPE_CHECKING:
from electrum.simple_config import SimpleConfig
@ -220,6 +221,22 @@ class QENewWalletWizard(NewWalletWizard, QEAbstractWizard, MessageBoxMixin):
if on_finished:
on_finished()
def query_choice(self, msg, choices, title=None, default_choice=None):
# Needed by QtHandler for hardware wallets
if title is None:
title = _('Question')
dialog = WindowModalDialog(self.top_level_window(), title=title)
dialog.setMinimumWidth(400)
clayout = ChoicesLayout(msg, choices, checked_index=default_choice)
vbox = QVBoxLayout(dialog)
vbox.addLayout(clayout.layout())
cancel_button = CancelButton(dialog)
vbox.addLayout(Buttons(cancel_button, OkButton(dialog)))
cancel_button.setFocus()
if not dialog.exec_():
return None
return clayout.selected_index()
class WCWalletName(WizardComponent, Logger):
def __init__(self, parent, wizard):
@ -1030,22 +1047,15 @@ class WCChooseHWDevice(WizardComponent, Logger):
self.device_list.setLayout(self.device_list_layout)
self.choice_w = None
self.rescan_button = QPushButton(_('Rescan devices'))
self.rescan_button.clicked.connect(self.on_rescan)
self.layout().addWidget(self.error_l)
self.layout().addWidget(self.device_list)
self.layout().addStretch(1)
self.layout().addWidget(self.rescan_button)
self.c_values = []
def on_ready(self):
self.scan_devices()
def on_rescan(self):
self.scan_devices()
def on_scan_failed(self, code, message):
self.error_l.setText(message)
self.error_l.setVisible(True)
@ -1080,8 +1090,6 @@ class WCChooseHWDevice(WizardComponent, Logger):
if self.valid:
self.wizard.next_button.setFocus()
else:
self.rescan_button.setFocus()
def failed_getting_device_infos(self, debug_msg, name, e):
# nonlocal debug_msg
@ -1141,8 +1149,7 @@ class WCChooseHWDevice(WizardComponent, Logger):
if not debug_msg:
debug_msg = ' {}'.format(_('No exceptions encountered.'))
if not devices:
msg = (_('No hardware device detected.') + '\n' +
_('To trigger a rescan, press \'Rescan devices\'.') + '\n\n')
msg = (_('No hardware device detected.') + '\n\n')
if sys.platform == 'win32':
msg += _('If your device is not detected on Windows, go to "Settings", "Devices", "Connected devices", '
'and do "Remove device". Then, plug your device again.') + '\n'

8
electrum/gui/qt/wizard/wizard.py

@ -22,6 +22,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin):
_logger = get_logger(__name__)
requestNext = pyqtSignal()
requestPrev = pyqtSignal()
def __init__(self, config: 'SimpleConfig', app: 'QElectrumApplication', *, start_viewstate: 'WizardViewState' = None):
QDialog.__init__(self, None)
@ -42,6 +43,7 @@ class QEAbstractWizard(QDialog, MessageBoxMixin):
self.next_button = QPushButton(_("Next"), self)
self.next_button.clicked.connect(self.on_next_button_clicked)
self.next_button.setDefault(True)
self.requestPrev.connect(self.on_back_button_clicked)
self.requestNext.connect(self.on_next_button_clicked)
self.logo = QLabel()
@ -57,9 +59,9 @@ class QEAbstractWizard(QDialog, MessageBoxMixin):
error_layout = QVBoxLayout()
error_layout.addStretch(1)
error_l = QLabel(_("Error!"))
error_l.setAlignment(Qt.AlignCenter)
error_layout.addWidget(error_l)
# error_l = QLabel(_("Error!"))
# error_l.setAlignment(Qt.AlignCenter)
# error_layout.addWidget(error_l)
self.error_msg = WWLabel()
self.error_msg.setAlignment(Qt.AlignCenter)
error_layout.addWidget(self.error_msg)

53
electrum/plugins/digitalbitbox/digitalbitbox.py

@ -35,6 +35,7 @@ from electrum.logging import get_logger
from electrum.plugin import runs_in_hwd_thread, run_in_hwd_thread
from ..hw_wallet import HW_PluginBase, HardwareClientBase, HardwareHandlerBase
from ..hw_wallet.plugin import OperationCancelled
if TYPE_CHECKING:
from electrum.plugin import DeviceInfo
@ -50,11 +51,14 @@ except ImportError as e:
DIGIBOX = False
class DeviceErased(UserFacingException):
pass
# ----------------------------------------------------------------------------------
# USB HID interface
#
def to_hexstr(s):
return binascii.hexlify(s).decode('ascii')
@ -64,6 +68,7 @@ def derive_keys(x):
h = hashlib.sha512(h).digest()
return (h[:32],h[32:])
MIN_MAJOR_VERSION = 5
ENCRYPTION_PRIVKEY_KEY = 'encryptionprivkey'
@ -78,7 +83,7 @@ class DigitalBitbox_Client(HardwareClientBase):
self.password = None
self.isInitialized = False
self.setupRunning = False
self.usbReportSize = 64 # firmware > v2.0.0
self.usbReportSize = 64 # firmware > v2.0.0
def device_model_name(self) -> Optional[str]:
return 'Digital BitBox'
@ -92,15 +97,12 @@ class DigitalBitbox_Client(HardwareClientBase):
pass
self.opened = False
def is_pairable(self):
return True
def is_initialized(self):
return self.dbb_has_password()
def is_paired(self):
return self.password is not None
@ -142,11 +144,9 @@ class DigitalBitbox_Client(HardwareClientBase):
return True
return False
def stretch_key(self, key: bytes):
return to_hexstr(hashlib.pbkdf2_hmac('sha512', key, b'Digital Bitbox', iterations = 20480))
def backup_password_dialog(self):
msg = _("Enter the password used when the backup was created:")
while True:
@ -162,7 +162,6 @@ class DigitalBitbox_Client(HardwareClientBase):
else:
return password.encode('utf8')
def password_dialog(self, msg):
while True:
password = self.handler.get_passphrase(msg, False)
@ -178,7 +177,7 @@ class DigitalBitbox_Client(HardwareClientBase):
self.password = password.encode('utf8')
return True
def check_device_dialog(self):
def check_firmware_version(self):
match = re.search(r'v([0-9])+\.[0-9]+\.[0-9]+',
run_in_hwd_thread(self.dbb_hid.get_serial_number_string))
if match is None:
@ -186,6 +185,9 @@ class DigitalBitbox_Client(HardwareClientBase):
major_version = int(match.group(1))
if major_version < MIN_MAJOR_VERSION:
raise Exception("Please upgrade to the newest firmware using the BitBox Desktop app: https://shiftcrypto.ch/start")
def check_device_dialog(self):
self.check_firmware_version()
# Set password if fresh device
if self.password is None and not self.dbb_has_password():
if not self.setupRunning:
@ -230,29 +232,23 @@ class DigitalBitbox_Client(HardwareClientBase):
self.mobile_pairing_dialog()
return self.isInitialized
def recover_or_erase_dialog(self):
msg = _("The Digital Bitbox is already seeded. Choose an option:") + "\n"
choices = [
(_("Create a wallet using the current seed")),
(_("Load a wallet from the micro SD card (the current seed is overwritten)")),
(_("Erase the Digital Bitbox"))
]
reply = self.handler.query_choice(msg, choices)
if reply is None:
return # user cancelled
if reply == 2:
raise UserCancelled()
if reply == 1:
self.dbb_erase()
elif reply == 1:
if not self.dbb_load_backup():
return
else:
if self.hid_send_encrypt(b'{"device":"info"}')['device']['lock']:
raise UserFacingException(_("Full 2FA enabled. This is not supported yet."))
# Use existing seed
self.isInitialized = True
def seed_device_dialog(self):
msg = _("Choose how to initialize your Digital Bitbox:") + "\n"
choices = [
@ -261,7 +257,7 @@ class DigitalBitbox_Client(HardwareClientBase):
]
reply = self.handler.query_choice(msg, choices)
if reply is None:
return # user cancelled
raise UserCancelled()
if reply == 0:
self.dbb_generate_wallet()
else:
@ -301,7 +297,7 @@ class DigitalBitbox_Client(HardwareClientBase):
]
reply = self.handler.query_choice(_('Mobile pairing options'), choices)
if reply is None:
return # user cancelled
raise UserCancelled()
if reply == 0:
if self.plugin.is_mobile_paired():
@ -321,7 +317,6 @@ class DigitalBitbox_Client(HardwareClientBase):
if 'error' in reply:
raise UserFacingException(reply['error']['message'])
def dbb_erase(self):
self.handler.show_message(_("Are you sure you want to erase the Digital Bitbox?") + "\n\n" +
_("To continue, touch the Digital Bitbox's light for 3 seconds.") + "\n\n" +
@ -329,11 +324,12 @@ class DigitalBitbox_Client(HardwareClientBase):
hid_reply = self.hid_send_encrypt(b'{"reset":"__ERASE__"}')
self.handler.finished()
if 'error' in hid_reply:
if hid_reply['error'].get('code') in (600, 601):
raise OperationCancelled()
raise UserFacingException(hid_reply['error']['message'])
else:
self.password = None
raise UserFacingException('Device erased')
raise DeviceErased('Device erased')
def dbb_load_backup(self, show_msg=True):
backups = self.hid_send_encrypt(b'{"backup":"list"}')
@ -341,10 +337,10 @@ class DigitalBitbox_Client(HardwareClientBase):
raise UserFacingException(backups['error']['message'])
f = self.handler.query_choice(_("Choose a backup file:"), backups['backup'])
if f is None:
return False # user cancelled
raise UserCancelled()
key = self.backup_password_dialog()
if key is None:
raise Exception('Canceled by user')
raise UserCancelled('No backup password provided')
key = self.stretch_key(key)
if show_msg:
self.handler.show_message(_("Loading backup...") + "\n\n" +
@ -354,6 +350,8 @@ class DigitalBitbox_Client(HardwareClientBase):
hid_reply = self.hid_send_encrypt(msg)
self.handler.finished()
if 'error' in hid_reply:
if hid_reply['error'].get('code') in (600, 601):
raise OperationCancelled()
raise UserFacingException(hid_reply['error']['message'])
return True
@ -459,11 +457,9 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
def give_error(self, message):
raise Exception(message)
def decrypt_message(self, pubkey, message, password):
raise RuntimeError(_('Encryption and decryption are currently not supported for {}').format(self.device))
def sign_message(self, sequence, message, password, *, script_type=None):
sig = None
try:
@ -519,12 +515,10 @@ class DigitalBitbox_KeyStore(Hardware_KeyStore):
else:
raise Exception(_("Could not sign message"))
except BaseException as e:
self.give_error(e)
return sig
def sign_transaction(self, tx, password):
if tx.is_complete():
return
@ -753,11 +747,9 @@ class DigitalBitboxPlugin(HW_PluginBase):
}
self.comserver_post_notification(verify_request_payload, handler=keystore.handler)
# new wizard
def wizard_entry_for_device(self, device_info: 'DeviceInfo', *, new_wallet=True) -> str:
if new_wallet:
return 'dbitbox_start' if device_info.initialized else 'dbitbox_not_initialized'
return 'dbitbox_start'
else:
return 'dbitbox_unlock'
@ -772,7 +764,6 @@ class DigitalBitboxPlugin(HW_PluginBase):
'accept': wizard.maybe_master_pubkey,
'last': lambda d: wizard.is_single_password() and wizard.last_cosigner(d)
},
'dbitbox_not_initialized': {},
'dbitbox_unlock': {
'last': True
},

66
electrum/plugins/digitalbitbox/qt.py

@ -1,14 +1,20 @@
import threading
from functools import partial
from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtSignal
from electrum.i18n import _
from electrum.plugin import hook
from electrum.wallet import Standard_Wallet, Abstract_Wallet
from electrum.util import UserCancelled, UserFacingException
from electrum.plugins.hw_wallet.qt import QtHandlerBase, QtPluginBase
from electrum.plugins.hw_wallet.plugin import only_hook_if_libraries_available, OperationCancelled
from ..hw_wallet.qt import QtHandlerBase, QtPluginBase
from ..hw_wallet.plugin import only_hook_if_libraries_available
from .digitalbitbox import DigitalBitboxPlugin
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWXPub, WCHWUninitialized, WCHWUnlock
from electrum.gui.qt.wizard.wallet import WCScriptAndDerivation, WCHWXPub, WCHWUnlock
from .digitalbitbox import DigitalBitboxPlugin, DeviceErased
if TYPE_CHECKING:
from electrum.gui.qt.wizard.wallet import QENewWalletWizard
@ -38,6 +44,7 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase):
addr = addrs[0]
if wallet.get_txin_type(addr) != 'p2pkh':
return
def show_address():
keystore.thread.add(partial(self.show_address, wallet, addr, keystore))
@ -51,15 +58,60 @@ class Plugin(DigitalBitboxPlugin, QtPluginBase):
def extend_wizard(self, wizard: 'QENewWalletWizard'):
super().extend_wizard(wizard)
views = {
'dbitbox_start': {'gui': WCScriptAndDerivation},
'dbitbox_start': {'gui': WCDigitalBitboxScriptAndDerivation},
'dbitbox_xpub': {'gui': WCHWXPub},
'dbitbox_not_initialized': {'gui': WCHWUninitialized},
'dbitbox_unlock': {'gui': WCHWUnlock}
}
wizard.navmap_merge(views)
class DigitalBitbox_Handler(QtHandlerBase):
def __init__(self, win):
super(DigitalBitbox_Handler, self).__init__(win, 'Digital Bitbox')
class WCDigitalBitboxScriptAndDerivation(WCScriptAndDerivation):
requestRecheck = pyqtSignal()
def __init__(self, parent, wizard):
WCScriptAndDerivation.__init__(self, parent, wizard)
self._busy = True
self.title = ''
self.client = None
self.requestRecheck.connect(self.check_device)
def on_ready(self):
super().on_ready()
_name, _info = self.wizard_data['hardware_device']
plugin = self.wizard.plugins.get_plugin(_info.plugin_name)
device_id = _info.device.id_
self.client = self.wizard.plugins.device_manager.client_by_id(device_id, scan_now=False)
if not self.client.handler:
self.client.handler = plugin.create_handler(self.wizard)
self.client.setupRunning = True
self.check_device()
def check_device(self):
self.error = None
self.busy = True
def check_task():
try:
self.client.check_device_dialog()
self.title = _('Script type and Derivation path')
self.valid = True
except (UserCancelled, OperationCancelled):
self.error = _('Cancelled')
self.wizard.requestPrev.emit()
except DeviceErased:
self.error = _('Device erased')
self.requestRecheck.emit()
except UserFacingException as e:
self.error = str(e)
finally:
self.busy = False
t = threading.Thread(target=check_task, daemon=True)
t.start()

4
electrum/plugins/hw_wallet/plugin.py

@ -361,3 +361,7 @@ class OutdatedHwFirmwareException(UserFacingException):
return str(self) + "\n\n" + suffix
else:
return suffix
class OperationCancelled(UserFacingException):
pass

Loading…
Cancel
Save