Skip to content

Commit 4a15909

Browse files
committed
bitbox01: implement update_firmware
1 parent 6ff4fe8 commit 4a15909

File tree

1 file changed

+136
-1
lines changed

1 file changed

+136
-1
lines changed

hwilib/devices/digitalbitbox.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Digital Bitbox interaction script
22

3+
import ecdsa
34
import hid
5+
import io
46
import struct
57
import json
68
import base64
@@ -18,9 +20,11 @@
1820
from ..hwwclient import HardwareWalletClient
1921
from ..errors import (
2022
ActionCanceledError,
23+
BAD_ARGUMENT,
2124
BadArgumentError,
2225
DeviceFailureError,
2326
DeviceAlreadyInitError,
27+
DEVICE_CONN_ERROR,
2428
DEVICE_NOT_INITIALIZED,
2529
DeviceNotReadyError,
2630
NoPasswordError,
@@ -121,6 +125,18 @@
121125

122126
ERR_MEM_SETUP = 503 # Device initialization in progress.
123127

128+
BITBOX01_FIRMWARE_KEYS = [
129+
"02a1137c6bdd497358537df77d1375a741ed75461b706a612a3717d32748e5acf1",
130+
"0256201125b958864de4bb00560a247ad246182866b6fe7ac29d7a12e7718ebb7d",
131+
"03d2185d70fb29a36691d8470e65d02adfab2ec00caad91887da23e5ad20a25163",
132+
"0263b742d9873405c609814da884324ab0f4c1597a5fd152b388899857f4d041df",
133+
"02b95dc22d293376222ef896f74a8436a8b6672e7e416299f3c4e23b49c38ad366",
134+
"03ef4c48dc308ace971c025db3edd4bc5d5110e28e14bdd925fffafd4d21002800",
135+
"030d8b0b86fca70bfd3a8d842cdb3ff8362c02f455fd092b080f1bb137dfc1d25f",
136+
]
137+
138+
EMPTY_SIG = b'\x00' * 64
139+
124140
class DBBError(Exception):
125141
def __init__(self, error):
126142
Exception.__init__(self)
@@ -199,6 +215,23 @@ def to_string(x, enc):
199215
else:
200216
raise DeviceFailureError("Not a string or bytes like object")
201217

218+
def verify_firmware(sig_blob, firmware):
219+
sigs = []
220+
for i in range(0, 448, 64):
221+
sigs.append(sig_blob[i:i + 64])
222+
fw_hash = hash256(firmware)
223+
print('Hashed firmware (without signatures) {}'.format(binascii.hexlify(fw_hash).decode()), file=sys.stderr)
224+
for i in range(0, 6):
225+
sig = sigs[i]
226+
pubkey_str = bytearray.fromhex(BITBOX01_FIRMWARE_KEYS[i])
227+
if sig == EMPTY_SIG:
228+
continue
229+
key = ecdsa.VerifyingKey.from_string(pubkey_str, curve=ecdsa.curves.SECP256k1)
230+
try:
231+
key.verify_digest(sig, fw_hash)
232+
except ecdsa.BadSignatureError:
233+
raise BadArgumentError("Invalid firmware signature at index {}".format(i))
234+
202235
class BitboxSimulator():
203236
def __init__(self, ip, port):
204237
self.ip = ip
@@ -218,6 +251,9 @@ def close(self):
218251
def get_serial_number_string(self):
219252
return 'dbb_fw:v5.0.0'
220253

254+
def get_product_string(self):
255+
return 'Digital Bitbox firmware'
256+
221257
def send_frame(data, device):
222258
data = bytearray(data)
223259
data_len = len(data)
@@ -323,6 +359,45 @@ def stretch_backup_key(password):
323359
def format_backup_filename(name):
324360
return '{}-{}.pdf'.format(name, time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime()))
325361

362+
# ----------------------------------------------------------------------------------
363+
# Bootloader io
364+
#
365+
366+
def sendBoot(msg, dev):
367+
msg = bytearray(msg) + b'\0' * (boot_buf_size_send - len(msg))
368+
serial_number = dev.get_serial_number_string()
369+
if 'v1.' in serial_number or 'v2.' in serial_number:
370+
dev.write(b'\0' + msg)
371+
else:
372+
# Split `msg` into 64-byte packets
373+
n = 0
374+
while n < len(msg):
375+
dev.write(b'\0' + msg[n:n + usb_report_size])
376+
n = n + usb_report_size
377+
378+
def sendPlainBoot(msg, dev):
379+
if type(msg) == str:
380+
msg = msg.encode()
381+
sendBoot(msg, dev)
382+
reply = []
383+
while len(reply) < boot_buf_size_reply:
384+
reply = reply + dev.read(boot_buf_size_reply)
385+
386+
reply = bytearray(reply).rstrip(b' \t\r\n\0')
387+
reply = ''.join(chr(e) for e in reply)
388+
return reply
389+
390+
def sendChunk(chunknum, data, dev):
391+
b = bytearray(b"\x77\x00")
392+
b[1] = chunknum % 0xFF
393+
b.extend(data)
394+
sendBoot(b, dev)
395+
reply = []
396+
while len(reply) < boot_buf_size_reply:
397+
reply = reply + dev.read(boot_buf_size_reply)
398+
reply = bytearray(reply).rstrip(b' \t\r\n\0')
399+
reply = ''.join(chr(e) for e in reply)
400+
326401
# This class extends the HardwareWalletClient for Digital Bitbox specific things
327402
class DigitalbitboxClient(HardwareWalletClient):
328403

@@ -340,6 +415,21 @@ def __init__(self, path, password, expert=False):
340415
self.device.open_path(path.encode())
341416
self.password = password
342417

418+
# Always lock the bootloader
419+
if self.device.get_product_string() != 'bootloader':
420+
reply = send_encrypt('{"device":"info"}', self.password, self.device)
421+
if 'error' not in reply:
422+
if not reply['device']['bootlock']:
423+
reply = send_encrypt('{"bootloader":"lock"}', self.password, self.device)
424+
if 'error' in reply:
425+
raise DBBError(reply)
426+
else:
427+
# Check it isn't initialized
428+
if reply['error']['code'] == 101 or reply['error']['code'] == '101':
429+
pass
430+
else:
431+
raise DBBError(reply)
432+
343433
# Must return a dict with the xpub
344434
# Retrieves the public key at the specified BIP 32 derivation path
345435
@digitalbitbox_exception
@@ -618,8 +708,53 @@ def toggle_passphrase(self):
618708
raise UnavailableActionError('The Digital Bitbox does not support toggling passphrase from the host')
619709

620710
# Verify firmware file then load it onto device
711+
@digitalbitbox_exception
621712
def update_firmware(self, filename: str) -> Dict[str, bool]:
622-
raise NotImplementedError('The Digital Bitbox does not implement this method yet')
713+
if self.device.get_product_string() != 'bootloader':
714+
print('Device is not in bootloader mode. Unlocking bootloader, replugging will be required', file=sys.stderr)
715+
print("Touch the device for 3 seconds to unlock bootloaderr. Touch briefly to cancel", file=sys.stderr)
716+
reply = send_encrypt('{"bootloader":"unlock"}', self.password, self.device)
717+
if 'error' in reply:
718+
raise DBBError(reply)
719+
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}
720+
721+
with open(filename, "rb") as f:
722+
data = bytearray()
723+
while True:
724+
d = f.read(chunksize)
725+
if len(d) == 0:
726+
break
727+
data = data + bytearray(d)
728+
data = data + b'\xFF' * (applen - len(data))
729+
firmware = data[448:]
730+
sig = data[:448]
731+
verify_firmware(sig, firmware)
732+
733+
sendPlainBoot("b", self.device) # blink led
734+
sendPlainBoot("v", self.device) # bootloader version
735+
sendPlainBoot("e", self.device) # erase existing firmware (required)
736+
737+
# Send firmware
738+
f = io.BytesIO(firmware)
739+
cnt = 0
740+
while True:
741+
chunk = f.read(chunksize)
742+
if len(chunk) == 0:
743+
break
744+
sendChunk(cnt, chunk, self.device)
745+
cnt += 1
746+
747+
# upload sigs and verify new firmware
748+
load_result = sendPlainBoot("s" + "0" + binascii.hexlify(sig).decode(), self.device)
749+
if load_result[1] == 'V':
750+
latest_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64:][:8]))
751+
app_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64 + 8:][:8]))
752+
return {'error': 'firmware downgrade not allowed. Got version %d, but must be equal or higher to %d' % (app_version, latest_version), 'code': BAD_ARGUMENT}
753+
elif load_result[1] != '0':
754+
return {'error': 'invalid firmware signature', 'code': BAD_ARGUMENT}
755+
756+
print('Please unplug and replug your device. The bootloader will be locked next time you use HWI with it.', file=sys.stderr)
757+
return {'success': True}
623758

624759
def enumerate(password=''):
625760
results = []

0 commit comments

Comments
 (0)