# -*- coding: utf-8 -*- # # Copyright 2019 SoloKeys Developers # # Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be # copied, modified, or distributed except according to those terms. import sys import time import click import usb from fido2.ctap import CtapError import solo from solo.dfu import hot_patch_windows_libusb @click.group() def program(): """Program a key.""" pass # @click.command() # def ctap(): # """Program via CTAP (either CTAP1 or CTAP2) (assumes Solo bootloader).""" # pass # program.add_command(ctap) @click.command() @click.option("-s", "--serial", help="serial number of DFU to use") @click.option( "-a", "--connect-attempts", default=8, help="number of times to attempt connecting" ) # @click.option("--attach", default=False, help="Attempt switching to DFU before starting") @click.option( "-d", "--detach", default=False, is_flag=True, help="Reboot after successful programming", ) @click.option("-n", "--dry-run", is_flag=True, help="Just attach and detach") @click.argument("firmware") # , help="firmware (bundle) to program") def dfu(serial, connect_attempts, detach, dry_run, firmware): """Program via STMicroelectronics DFU interface. Enter dfu mode using `solo1 program aux enter-dfu` first. """ import time import usb.core from intelhex import IntelHex dfu = solo.dfu.find(serial, attempts=connect_attempts) if dfu is None: print("No STU DFU device found.") if serial is not None: print("Serial number used: ", serial) sys.exit(1) dfu.init() if not dry_run: # The actual programming # TODO: move to `operations.py` or elsewhere ih = IntelHex() ih.fromfile(firmware, format="hex") chunk = 2048 # Why is this unused # seg = ih.segments()[0] size = sum([max(x[1] - x[0], chunk) for x in ih.segments()]) total = 0 t1 = time.time() * 1000 print("erasing...") try: dfu.mass_erase() except usb.core.USBError: # garbage write, sometimes needed before mass_erase dfu.write_page(0x08000000 + 2048 * 10, "ZZFF" * (2048 // 4)) dfu.mass_erase() page = 0 for start, end in ih.segments(): for i in range(start, end, chunk): page += 1 data = ih.tobinarray(start=i, size=chunk) dfu.write_page(i, data) total += chunk # here and below, progress would overshoot 100% otherwise progress = min(100, total / float(size) * 100) sys.stdout.write( "downloading %.2f%% %08x - %08x ... \r" % (progress, i, i + page) ) # time.sleep(0.100) # print('done') # print(dfu.read_mem(i,16)) t2 = time.time() * 1000 print() print("time: %d ms" % (t2 - t1)) print("verifying...") progress = 0 for start, end in ih.segments(): for i in range(start, end, chunk): data1 = dfu.read_mem(i, 2048) data2 = ih.tobinarray(start=i, size=chunk) total += chunk progress = min(100, total / float(size) * 100) sys.stdout.write( "reading %.2f%% %08x - %08x ... \r" % (progress, i, i + page) ) if (end - start) == chunk: assert data1 == data2 print() print("firmware readback verified.") if detach: dfu.prepare_options_bytes_detach() dfu.detach() print("Please powercycle the device (pull out, plug in again)") hot_patch_windows_libusb() program.add_command(dfu) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to use") @click.argument("firmware") # , help="firmware (bundle) to program") def bootloader(serial, firmware): """Program via Solo bootloader interface. \b FIRMWARE argument should be either a .hex or .json file. If the bootloader is verifying, the .json is needed containing a signature for the verifying key in the bootloader. If the bootloader is nonverifying, either .hex or .json can be used. DANGER: if you try to flash a firmware with signature that doesn't match the bootloader's verifying key, you will be stuck in bootloader mode until you find a signed firmware that does match. Enter bootloader mode using `solo1 program aux enter-bootloader` first. """ p = solo.client.find(serial) try: p.use_hid() p.program_file(firmware) except CtapError as e: if e.code == CtapError.ERR.INVALID_COMMAND: print("Not in bootloader mode. Attempting to switch...") else: raise e p.enter_bootloader_or_die() print("Solo rebooted. Reconnecting...") time.sleep(0.5) p = solo.client.find(serial) if p is None: print("Cannot find Solo device.") sys.exit(1) p.use_hid() p.program_file(firmware) program.add_command(bootloader) @click.group() def aux(): """Auxiliary commands related to firmware/bootloader/dfu mode.""" pass program.add_command(aux) def _enter_bootloader(serial): p = solo.client.find(serial) p.enter_bootloader_or_die() print("Solo rebooted. Reconnecting...") time.sleep(0.5) if solo.client.find(serial) is None: raise RuntimeError("Failed to reconnect!") @click.command() @click.option("-s", "--serial", help="Serial number of Solo to use") def enter_bootloader(serial): """Switch from Solo firmware to Solo bootloader. Note that after powercycle, you will be in the firmware again, assuming it is valid. """ return _enter_bootloader(serial) aux.add_command(enter_bootloader) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to use") def leave_bootloader(serial): """Switch from Solo bootloader to Solo firmware.""" p = solo.client.find(serial) # this is a bit too low-level... # p.exchange(solo.commands.SoloBootloader.done, 0, b"A" * 64) p.reboot() aux.add_command(leave_bootloader) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to use") def enter_dfu(serial): """Switch from Solo bootloader to ST DFU bootloader. This changes the boot options of the key, which only reliably take effect after a powercycle. """ p = solo.client.find(serial) try: p.enter_st_dfu() print("Please powercycle the device (pull out, plug in again)") except Exception as e: if "wrong channel" in str(e).lower(): print( "Command wasn't accepted by Solo. It must be in bootloader mode first and be a 'hacker' device." ) aux.add_command(enter_dfu) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to use") def leave_dfu(serial): """Leave ST DFU bootloader. Switches to Solo bootloader or firmware, latter if firmware is valid. This changes the boot options of the key, which only reliably take effect after a powercycle. """ dfu = solo.dfu.find(serial) # select option bytes dfu.init() dfu.prepare_options_bytes_detach() try: dfu.detach() except usb.core.USBError: pass hot_patch_windows_libusb() print("Please powercycle the device (pull out, plug in again)") aux.add_command(leave_dfu) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to use") def reboot(serial): """Reboot. \b This should reboot from anything (firmware, bootloader, DFU). Separately, need to be able to set boot options. """ # this implementation actually only works for bootloader # firmware doesn't have a reboot command solo.client.find(serial).reboot() aux.add_command(reboot) @click.command() @click.option("-s", "--serial", help="Serial number of Solo to use") def bootloader_version(serial): """Version of bootloader.""" p = solo.client.find(serial) print(".".join(map(str, p.bootloader_version()))) aux.add_command(bootloader_version)