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.
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
},
|
||||
|
||||
@@ -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 ..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.plugins.hw_wallet.qt import QtHandlerBase, QtPluginBase
|
||||
from electrum.plugins.hw_wallet.plugin import only_hook_if_libraries_available, OperationCancelled
|
||||
|
||||
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()
|
||||
|
||||
@@ -361,3 +361,7 @@ class OutdatedHwFirmwareException(UserFacingException):
|
||||
return str(self) + "\n\n" + suffix
|
||||
else:
|
||||
return suffix
|
||||
|
||||
|
||||
class OperationCancelled(UserFacingException):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user