Skip to content

Commit d1fb85b

Browse files
committed
bitbox01: implement update_firmware
1 parent 6e5a325 commit d1fb85b

File tree

1 file changed

+105
-2
lines changed

1 file changed

+105
-2
lines changed

hwilib/devices/digitalbitbox.py

Lines changed: 105 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Digital Bitbox interaction script
22

33
import hid
4+
import io
45
import struct
56
import json
67
import base64
@@ -15,7 +16,7 @@
1516
import time
1617

1718
from ..hwwclient import HardwareWalletClient
18-
from ..errors import ActionCanceledError, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DEVICE_NOT_INITIALIZED, DeviceNotReadyError, NoPasswordError, UnavailableActionError, common_err_msgs, handle_errors
19+
from ..errors import ActionCanceledError, BAD_ARGUMENT, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DEVICE_CONN_ERROR, DEVICE_NOT_INITIALIZED, DeviceNotReadyError, NoPasswordError, UnavailableActionError, common_err_msgs, handle_errors
1920
from ..serializations import CTransaction, ExtendedKey, hash256, ser_sig_der, ser_sig_compact, ser_compact_size
2021
from ..base58 import get_xpub_fingerprint, xpub_main_2_test, get_xpub_fingerprint_hex
2122

@@ -188,6 +189,9 @@ def close(self):
188189
def get_serial_number_string(self):
189190
return 'dbb_fw:v5.0.0'
190191

192+
def get_product_string(self):
193+
return 'Digital Bitbox firmware'
194+
191195
def send_frame(data, device):
192196
data = bytearray(data)
193197
data_len = len(data)
@@ -293,6 +297,45 @@ def stretch_backup_key(password):
293297
def format_backup_filename(name):
294298
return '{}-{}.pdf'.format(name, time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime()))
295299

300+
# ----------------------------------------------------------------------------------
301+
# Bootloader io
302+
#
303+
304+
def sendBoot(msg, dev):
305+
msg = bytearray(msg) + b'\0' * (boot_buf_size_send - len(msg))
306+
serial_number = dev.get_serial_number_string()
307+
if 'v1.' in serial_number or 'v2.' in serial_number:
308+
dev.write(b'\0' + msg)
309+
else:
310+
# Split `msg` into 64-byte packets
311+
n = 0
312+
while n < len(msg):
313+
dev.write(b'\0' + msg[n:n + usb_report_size])
314+
n = n + usb_report_size
315+
316+
def sendPlainBoot(msg, dev):
317+
if type(msg) == str:
318+
msg = msg.encode()
319+
sendBoot(msg, dev)
320+
reply = []
321+
while len(reply) < boot_buf_size_reply:
322+
reply = reply + dev.read(boot_buf_size_reply)
323+
324+
reply = bytearray(reply).rstrip(b' \t\r\n\0')
325+
reply = ''.join(chr(e) for e in reply)
326+
return reply
327+
328+
def sendChunk(chunknum, data, dev):
329+
b = bytearray(b"\x77\x00")
330+
b[1] = chunknum % 0xFF
331+
b.extend(data)
332+
sendBoot(b, dev)
333+
reply = []
334+
while len(reply) < boot_buf_size_reply:
335+
reply = reply + dev.read(boot_buf_size_reply)
336+
reply = bytearray(reply).rstrip(b' \t\r\n\0')
337+
reply = ''.join(chr(e) for e in reply)
338+
296339
# This class extends the HardwareWalletClient for Digital Bitbox specific things
297340
class DigitalbitboxClient(HardwareWalletClient):
298341

@@ -310,6 +353,21 @@ def __init__(self, path, password, expert=False):
310353
self.device.open_path(path.encode())
311354
self.password = password
312355

356+
# Always lock the bootloader
357+
if self.device.get_product_string() != 'bootloader':
358+
reply = send_encrypt('{"device":"info"}', self.password, self.device)
359+
if 'error' not in reply:
360+
if not reply['device']['bootlock']:
361+
reply = send_encrypt('{"bootloader":"lock"}', self.password, self.device)
362+
if 'error' in reply:
363+
raise DBBError(reply)
364+
else:
365+
# Check it isn't initialized
366+
if reply['error']['code'] == 101 or reply['error']['code'] == '101':
367+
pass
368+
else:
369+
raise DBBError(reply)
370+
313371
# Must return a dict with the xpub
314372
# Retrieves the public key at the specified BIP 32 derivation path
315373
@digitalbitbox_exception
@@ -584,8 +642,53 @@ def send_pin(self, pin):
584642
raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host')
585643

586644
# Verify firmware file then load it onto device
645+
@digitalbitbox_exception
587646
def update_firmware(self, file):
588-
raise NotImplementedError('The Digital Bitbox does not implement this method yet')
647+
if self.device.get_product_string() != 'bootloader':
648+
print('Device is not in bootloader mode. Unlocking bootloader, replugging will be required', file=sys.stderr)
649+
print("Touch the device for 3 seconds to unlock bootloaderr. Touch briefly to cancel", file=sys.stderr)
650+
reply = send_encrypt('{"bootloader":"unlock"}', self.password, self.device)
651+
if 'error' in reply:
652+
raise DBBError(reply)
653+
return {'error': 'Digital Bitbox needs to be in bootloader mode. Unplug and replug the device and briefly touch the button within 3 seconds. Then try this command again', 'code': DEVICE_CONN_ERROR}
654+
655+
with open(file, "rb") as f:
656+
data = bytearray()
657+
while True:
658+
d = f.read(chunksize)
659+
if len(d) == 0:
660+
break
661+
data = data + bytearray(d)
662+
data = data + b'\xFF' * (applen - len(data))
663+
firmware = data[448:]
664+
sig = data[:448]
665+
print('Hashed firmware (without signatures)', binascii.hexlify(hash256((firmware))), file=sys.stderr)
666+
667+
sendPlainBoot("b", self.device) # blink led
668+
sendPlainBoot("v", self.device) # bootloader version
669+
sendPlainBoot("e", self.device) # erase existing firmware (required)
670+
671+
# Send firmware
672+
f = io.BytesIO(firmware)
673+
cnt = 0
674+
while True:
675+
chunk = f.read(chunksize)
676+
if len(chunk) == 0:
677+
break
678+
sendChunk(cnt, chunk, self.device)
679+
cnt += 1
680+
681+
# upload sigs and verify new firmware
682+
load_result = sendPlainBoot("s" + "0" + binascii.hexlify(sig).decode(), self.device)
683+
if load_result[1] == 'V':
684+
latest_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64:][:8]))
685+
app_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64 + 8:][:8]))
686+
return {'error': 'firmware downgrade not allowed. Got version %d, but must be equal or higher to %d' % (app_version, latest_version), 'code': BAD_ARGUMENT}
687+
elif load_result[1] != '0':
688+
return {'error': 'invalid firmware signature', 'code': BAD_ARGUMENT}
689+
690+
print('Please unplug and replug your device. The bootloader will be locked next time you use HWI with it.', file=sys.stderr)
691+
return {'success': True}
589692

590693
def enumerate(password=''):
591694
results = []

0 commit comments

Comments
 (0)