11# Digital Bitbox interaction script
22
3+ import ecdsa
34import hid
5+ import io
46import struct
57import json
68import base64
1517import time
1618
1719from ..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
1921from ..serializations import CTransaction , ExtendedKey , hash256 , ser_sig_der , ser_sig_compact , ser_compact_size
2022from ..base58 import get_xpub_fingerprint , xpub_main_2_test , get_xpub_fingerprint_hex
2123
9193
9294ERR_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+
94108class 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+
172203class 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+
191225def send_frame (data , device ):
192226 data = bytearray (data )
193227 data_len = len (data )
@@ -293,6 +327,45 @@ def stretch_backup_key(password):
293327def 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
297370class 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
590723def enumerate (password = '' ):
591724 results = []
0 commit comments