Skip to content

Commit 22e4364

Browse files
committed
bitbox01: implement update_firmware
1 parent c9aa65c commit 22e4364

File tree

1 file changed

+135
-2
lines changed

1 file changed

+135
-2
lines changed

hwilib/devices/digitalbitbox.py

Lines changed: 135 additions & 2 deletions
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
@@ -15,7 +17,7 @@
1517
import time
1618

1719
from ..hwwclient import HardwareWalletClient
18-
from ..errors import ActionCanceledError, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DEVICE_NOT_INITIALIZED, DeviceNotReadyError, NoPasswordError, UnavailableActionError, common_err_msgs, handle_errors
20+
from ..errors import ActionCanceledError, BAD_ARGUMENT, BadArgumentError, DeviceFailureError, DeviceAlreadyInitError, DEVICE_CONN_ERROR, DEVICE_NOT_INITIALIZED, DeviceNotReadyError, NoPasswordError, UnavailableActionError, common_err_msgs, handle_errors
1921
from ..serializations import CTransaction, ExtendedKey, hash256, ser_sig_der, ser_sig_compact, ser_compact_size
2022
from ..base58 import get_xpub_fingerprint, xpub_main_2_test, get_xpub_fingerprint_hex
2123

@@ -91,6 +93,18 @@
9193

9294
ERR_MEM_SETUP = 503 # Device initialization in progress.
9395

96+
BITBOX01_FIRMWARE_KEYS = [
97+
"02a1137c6bdd497358537df77d1375a741ed75461b706a612a3717d32748e5acf1",
98+
"0256201125b958864de4bb00560a247ad246182866b6fe7ac29d7a12e7718ebb7d",
99+
"03d2185d70fb29a36691d8470e65d02adfab2ec00caad91887da23e5ad20a25163",
100+
"0263b742d9873405c609814da884324ab0f4c1597a5fd152b388899857f4d041df",
101+
"02b95dc22d293376222ef896f74a8436a8b6672e7e416299f3c4e23b49c38ad366",
102+
"03ef4c48dc308ace971c025db3edd4bc5d5110e28e14bdd925fffafd4d21002800",
103+
"030d8b0b86fca70bfd3a8d842cdb3ff8362c02f455fd092b080f1bb137dfc1d25f",
104+
]
105+
106+
EMPTY_SIG = b'\x00' * 64
107+
94108
class DBBError(Exception):
95109
def __init__(self, error):
96110
Exception.__init__(self)
@@ -169,6 +183,23 @@ def to_string(x, enc):
169183
else:
170184
raise DeviceFailureError("Not a string or bytes like object")
171185

186+
def verify_firmware(sig_blob, firmware):
187+
sigs = []
188+
for i in range(0, 448, 64):
189+
sigs.append(sig_blob[i:i + 64])
190+
fw_hash = hash256(firmware)
191+
print('Hashed firmware (without signatures) {}'.format(binascii.hexlify(fw_hash).decode()), file=sys.stderr)
192+
for i in range(0, 6):
193+
sig = sigs[i]
194+
pubkey_str = bytearray.fromhex(BITBOX01_FIRMWARE_KEYS[i])
195+
if sig == EMPTY_SIG:
196+
continue
197+
key = ecdsa.VerifyingKey.from_string(pubkey_str, curve=ecdsa.curves.SECP256k1)
198+
try:
199+
key.verify_digest(sig, fw_hash)
200+
except ecdsa.BadSignatureError:
201+
raise BadArgumentError("Invalid firmware signature at index {}".format(i))
202+
172203
class BitboxSimulator():
173204
def __init__(self, ip, port):
174205
self.ip = ip
@@ -188,6 +219,9 @@ def close(self):
188219
def get_serial_number_string(self):
189220
return 'dbb_fw:v5.0.0'
190221

222+
def get_product_string(self):
223+
return 'Digital Bitbox firmware'
224+
191225
def send_frame(data, device):
192226
data = bytearray(data)
193227
data_len = len(data)
@@ -293,6 +327,45 @@ def stretch_backup_key(password):
293327
def format_backup_filename(name):
294328
return '{}-{}.pdf'.format(name, time.strftime('%Y-%m-%d-%H-%M-%S', time.localtime()))
295329

330+
# ----------------------------------------------------------------------------------
331+
# Bootloader io
332+
#
333+
334+
def sendBoot(msg, dev):
335+
msg = bytearray(msg) + b'\0' * (boot_buf_size_send - len(msg))
336+
serial_number = dev.get_serial_number_string()
337+
if 'v1.' in serial_number or 'v2.' in serial_number:
338+
dev.write(b'\0' + msg)
339+
else:
340+
# Split `msg` into 64-byte packets
341+
n = 0
342+
while n < len(msg):
343+
dev.write(b'\0' + msg[n:n + usb_report_size])
344+
n = n + usb_report_size
345+
346+
def sendPlainBoot(msg, dev):
347+
if type(msg) == str:
348+
msg = msg.encode()
349+
sendBoot(msg, dev)
350+
reply = []
351+
while len(reply) < boot_buf_size_reply:
352+
reply = reply + dev.read(boot_buf_size_reply)
353+
354+
reply = bytearray(reply).rstrip(b' \t\r\n\0')
355+
reply = ''.join(chr(e) for e in reply)
356+
return reply
357+
358+
def sendChunk(chunknum, data, dev):
359+
b = bytearray(b"\x77\x00")
360+
b[1] = chunknum % 0xFF
361+
b.extend(data)
362+
sendBoot(b, dev)
363+
reply = []
364+
while len(reply) < boot_buf_size_reply:
365+
reply = reply + dev.read(boot_buf_size_reply)
366+
reply = bytearray(reply).rstrip(b' \t\r\n\0')
367+
reply = ''.join(chr(e) for e in reply)
368+
296369
# This class extends the HardwareWalletClient for Digital Bitbox specific things
297370
class DigitalbitboxClient(HardwareWalletClient):
298371

@@ -310,6 +383,21 @@ def __init__(self, path, password, expert=False):
310383
self.device.open_path(path.encode())
311384
self.password = password
312385

386+
# Always lock the bootloader
387+
if self.device.get_product_string() != 'bootloader':
388+
reply = send_encrypt('{"device":"info"}', self.password, self.device)
389+
if 'error' not in reply:
390+
if not reply['device']['bootlock']:
391+
reply = send_encrypt('{"bootloader":"lock"}', self.password, self.device)
392+
if 'error' in reply:
393+
raise DBBError(reply)
394+
else:
395+
# Check it isn't initialized
396+
if reply['error']['code'] == 101 or reply['error']['code'] == '101':
397+
pass
398+
else:
399+
raise DBBError(reply)
400+
313401
# Must return a dict with the xpub
314402
# Retrieves the public key at the specified BIP 32 derivation path
315403
@digitalbitbox_exception
@@ -584,8 +672,53 @@ def send_pin(self, pin):
584672
raise UnavailableActionError('The Digital Bitbox does not need a PIN sent from the host')
585673

586674
# Verify firmware file then load it onto device
675+
@digitalbitbox_exception
587676
def update_firmware(self, file):
588-
raise NotImplementedError('The Digital Bitbox does not implement this method yet')
677+
if self.device.get_product_string() != 'bootloader':
678+
print('Device is not in bootloader mode. Unlocking bootloader, replugging will be required', file=sys.stderr)
679+
print("Touch the device for 3 seconds to unlock bootloaderr. Touch briefly to cancel", file=sys.stderr)
680+
reply = send_encrypt('{"bootloader":"unlock"}', self.password, self.device)
681+
if 'error' in reply:
682+
raise DBBError(reply)
683+
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}
684+
685+
with open(file, "rb") as f:
686+
data = bytearray()
687+
while True:
688+
d = f.read(chunksize)
689+
if len(d) == 0:
690+
break
691+
data = data + bytearray(d)
692+
data = data + b'\xFF' * (applen - len(data))
693+
firmware = data[448:]
694+
sig = data[:448]
695+
verify_firmware(sig, firmware)
696+
697+
sendPlainBoot("b", self.device) # blink led
698+
sendPlainBoot("v", self.device) # bootloader version
699+
sendPlainBoot("e", self.device) # erase existing firmware (required)
700+
701+
# Send firmware
702+
f = io.BytesIO(firmware)
703+
cnt = 0
704+
while True:
705+
chunk = f.read(chunksize)
706+
if len(chunk) == 0:
707+
break
708+
sendChunk(cnt, chunk, self.device)
709+
cnt += 1
710+
711+
# upload sigs and verify new firmware
712+
load_result = sendPlainBoot("s" + "0" + binascii.hexlify(sig).decode(), self.device)
713+
if load_result[1] == 'V':
714+
latest_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64:][:8]))
715+
app_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64 + 8:][:8]))
716+
return {'error': 'firmware downgrade not allowed. Got version %d, but must be equal or higher to %d' % (app_version, latest_version), 'code': BAD_ARGUMENT}
717+
elif load_result[1] != '0':
718+
return {'error': 'invalid firmware signature', 'code': BAD_ARGUMENT}
719+
720+
print('Please unplug and replug your device. The bootloader will be locked next time you use HWI with it.', file=sys.stderr)
721+
return {'success': True}
589722

590723
def enumerate(password=''):
591724
results = []

0 commit comments

Comments
 (0)