1
1
# Digital Bitbox interaction script
2
2
3
+ import ecdsa
3
4
import hid
5
+ import io
4
6
import struct
5
7
import json
6
8
import base64
15
17
import time
16
18
17
19
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
19
21
from ..serializations import CTransaction , ExtendedKey , hash256 , ser_sig_der , ser_sig_compact , ser_compact_size
20
22
from ..base58 import get_xpub_fingerprint , xpub_main_2_test , get_xpub_fingerprint_hex
21
23
91
93
92
94
ERR_MEM_SETUP = 503 # Device initialization in progress.
93
95
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
+
94
108
class DBBError (Exception ):
95
109
def __init__ (self , error ):
96
110
Exception .__init__ (self )
@@ -169,6 +183,23 @@ def to_string(x, enc):
169
183
else :
170
184
raise DeviceFailureError ("Not a string or bytes like object" )
171
185
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
+
172
203
class BitboxSimulator ():
173
204
def __init__ (self , ip , port ):
174
205
self .ip = ip
@@ -188,6 +219,9 @@ def close(self):
188
219
def get_serial_number_string (self ):
189
220
return 'dbb_fw:v5.0.0'
190
221
222
+ def get_product_string (self ):
223
+ return 'Digital Bitbox firmware'
224
+
191
225
def send_frame (data , device ):
192
226
data = bytearray (data )
193
227
data_len = len (data )
@@ -293,6 +327,45 @@ def stretch_backup_key(password):
293
327
def format_backup_filename (name ):
294
328
return '{}-{}.pdf' .format (name , time .strftime ('%Y-%m-%d-%H-%M-%S' , time .localtime ()))
295
329
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
+
296
369
# This class extends the HardwareWalletClient for Digital Bitbox specific things
297
370
class DigitalbitboxClient (HardwareWalletClient ):
298
371
@@ -310,6 +383,21 @@ def __init__(self, path, password, expert=False):
310
383
self .device .open_path (path .encode ())
311
384
self .password = password
312
385
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
+
313
401
# Must return a dict with the xpub
314
402
# Retrieves the public key at the specified BIP 32 derivation path
315
403
@digitalbitbox_exception
@@ -584,8 +672,53 @@ def send_pin(self, pin):
584
672
raise UnavailableActionError ('The Digital Bitbox does not need a PIN sent from the host' )
585
673
586
674
# Verify firmware file then load it onto device
675
+ @digitalbitbox_exception
587
676
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 }
589
722
590
723
def enumerate (password = '' ):
591
724
results = []
0 commit comments