File: program.py

package info (click to toggle)
solo1-cli 0.1.1-6
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 464 kB
  • sloc: python: 2,168; makefile: 36
file content (314 lines) | stat: -rw-r--r-- 8,563 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
# -*- coding: utf-8 -*-
#
# Copyright 2019 SoloKeys Developers
#
# Licensed under the Apache License, Version 2.0, <LICENSE-APACHE or
# http://apache.org/licenses/LICENSE-2.0> or the MIT license <LICENSE-MIT or
# http://opensource.org/licenses/MIT>, 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)