1
0

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:
Sander van Grieken
2023-09-08 15:44:13 +02:00
parent 03435ebdbe
commit b7612605c5
5 changed files with 109 additions and 53 deletions

View File

@@ -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
},

View File

@@ -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()