11# Digital Bitbox interaction script
22
3+ import ecdsa
34import hid
5+ import io
46import struct
57import json
68import base64
1820from ..hwwclient import HardwareWalletClient
1921from ..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 ,
121125
122126ERR_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+
124140class 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+
202235class 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+
221257def send_frame (data , device ):
222258 data = bytearray (data )
223259 data_len = len (data )
@@ -323,6 +359,45 @@ def stretch_backup_key(password):
323359def 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
327402class 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
624759def enumerate (password = '' ):
625760 results = []
0 commit comments