Skip to content

Commit dedc2a2

Browse files
committed
bitbox01: implement update_firmware
1 parent 2b7596c commit dedc2a2

File tree

1 file changed

+97
-2
lines changed

1 file changed

+97
-2
lines changed

hwilib/devices/digitalbitbox.py

Lines changed: 97 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, 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

@@ -293,6 +294,45 @@ def stretch_backup_key(password):
293294
def format_backup_filename(name):
294295
return '{}-{}.pdf'.format(name, time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime()))
295296

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

@@ -310,6 +350,16 @@ def __init__(self, path, password):
310350
self.device.open_path(path.encode())
311351
self.password = password
312352

353+
# Always lock the bootloader
354+
if self.device.get_product_string() != 'bootloader':
355+
reply = send_encrypt('{"device":"info"}', self.password, self.device)
356+
if 'error' in reply:
357+
raise DBBError(reply)
358+
if not reply['device']['bootlock']:
359+
reply = send_encrypt('{"bootloader":"lock"}', self.password, self.device)
360+
if 'error' in reply:
361+
raise DBBError(reply)
362+
313363
# Must return a dict with the xpub
314364
# Retrieves the public key at the specified BIP 32 derivation path
315365
@digitalbitbox_exception
@@ -579,8 +629,53 @@ def send_pin(self, pin):
579629
raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host')
580630

581631
# Verify firmware file then load it onto device
632+
@digitalbitbox_exception
582633
def update_firmware(self, file):
583-
raise NotImplementedError('The Digital Bitbox does not implement this method yet')
634+
if self.device.get_product_string() != 'bootloader':
635+
print('Device is not in bootloader mode. Unlocking bootloader, replugging will be required', file=sys.stderr)
636+
print("Touch the device for 3 seconds to unlock bootloaderr. Touch briefly to cancel", file=sys.stderr)
637+
reply = send_encrypt('{"bootloader":"unlock"}', self.password, self.device)
638+
if 'error' in reply:
639+
raise DBBError(reply)
640+
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}
641+
642+
with open(file, "rb") as f:
643+
data = bytearray()
644+
while True:
645+
d = f.read(chunksize)
646+
if len(d) == 0:
647+
break
648+
data = data + bytearray(d)
649+
data = data + b'\xFF' * (applen - len(data))
650+
firmware = data[448:]
651+
sig = data[:448]
652+
print('Hashed firmware (without signatures)', binascii.hexlify(hash256((firmware))), file=sys.stderr)
653+
654+
sendPlainBoot("b", self.device) # blink led
655+
sendPlainBoot("v", self.device) # bootloader version
656+
sendPlainBoot("e", self.device) # erase existing firmware (required)
657+
658+
# Send firmware
659+
f = io.BytesIO(firmware)
660+
cnt = 0
661+
while True:
662+
chunk = f.read(chunksize)
663+
if len(chunk) == 0:
664+
break
665+
sendChunk(cnt, chunk, self.device)
666+
cnt += 1
667+
668+
# upload sigs and verify new firmware
669+
load_result = sendPlainBoot("s" + "0" + binascii.hexlify(sig).decode(), self.device)
670+
if load_result[1] == 'V':
671+
latest_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64:][:8]))
672+
app_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64 + 8:][:8]))
673+
return {'error': 'firmware downgrade not allowed. Got version %d, but must be equal or higher to %d' % (app_version, latest_version), 'code': BAD_ARGUMENT}
674+
elif load_result[1] != '0':
675+
return {'error': 'invalid firmware signature', 'code': BAD_ARGUMENT}
676+
677+
print('Please unplug and replug your device. The bootloader will be locked next time you use HWI with it.', file=sys.stderr)
678+
return {'success': True}
584679

585680
def enumerate(password=''):
586681
results = []

0 commit comments

Comments
 (0)