diff --git a/README.md b/README.md index 9505035db..000b39087 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,9 @@ Please also see [docs](docs/) for additional information about each device. | Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes | | Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes | | Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes | +| Firmware Downloading | N/A | N/A | Yes | Yes | Yes | No | Yes | Yes | +| Firmware Sig Verify | N/A | N/A | Yes | Yes | Yes | No |Yes | Yes | +| Firmware Upgrade | N/A | N/A | Yes | Yes | Yes | No | Yes | Yes | ## Using with Bitcoin Core diff --git a/contrib/build_bin.sh b/contrib/build_bin.sh index a96c72697..c4e81a5cc 100755 --- a/contrib/build_bin.sh +++ b/contrib/build_bin.sh @@ -27,6 +27,7 @@ export PYTHONHASHSEED=42 poetry run pyinstaller hwi.spec poetry run contrib/generate-ui.sh poetry run pyinstaller hwi-qt.spec +poetry run pyinstaller firmwaredl.spec unset PYTHONHASHSEED # Make the final compressed package @@ -36,5 +37,5 @@ OS=`uname | tr '[:upper:]' '[:lower:]'` if [[ $OS == "darwin" ]]; then OS="mac" fi -tar -czf "hwi-${VERSION}-${OS}-amd64.tar.gz" hwi hwi-qt +tar -czf "hwi-${VERSION}-${OS}-amd64.tar.gz" hwi hwi-qt firmwaredl popd diff --git a/contrib/build_wine.sh b/contrib/build_wine.sh index c09a67b58..bbc775e48 100755 --- a/contrib/build_wine.sh +++ b/contrib/build_wine.sh @@ -83,10 +83,11 @@ popd export PYTHONHASHSEED=42 $POETRY run pyinstaller hwi.spec $POETRY run pyinstaller hwi-qt.spec +$POETRY run pyinstaller firmwaredl.spec unset PYTHONHASHSEED # Make the final compressed package pushd dist VERSION=`$POETRY run hwi --version | cut -d " " -f 2 | dos2unix` -zip "hwi-${VERSION}-windows-amd64.zip" hwi.exe hwi-qt.exe +zip "hwi-${VERSION}-windows-amd64.zip" hwi.exe hwi-qt.exe firmwaredl.exe popd diff --git a/firmwaredl.py b/firmwaredl.py new file mode 100755 index 000000000..ad0591aec --- /dev/null +++ b/firmwaredl.py @@ -0,0 +1,9 @@ +#! /usr/bin/env python3 + +# Firmware downloader script + +if __name__ == '__main__': + from hwilib.firmware import main + main() +else: + raise ImportError('firmwaredl is not importable') diff --git a/firmwaredl.spec b/firmwaredl.spec new file mode 100644 index 000000000..d9e44a04e --- /dev/null +++ b/firmwaredl.spec @@ -0,0 +1,32 @@ +# -*- mode: python ; coding: utf-8 -*- + +block_cipher = None + + +a = Analysis(['firmwaredl.py'], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=['contrib/pyinstaller-hooks/'], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False) +pyz = PYZ(a.pure, a.zipped_data, + cipher=block_cipher) +exe = EXE(pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='firmwaredl', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True ) diff --git a/hwilib/cli.py b/hwilib/cli.py index fd25d2a31..bb539081c 100644 --- a/hwilib/cli.py +++ b/hwilib/cli.py @@ -17,6 +17,7 @@ setup_device, signmessage, signtx, + update_firmware, wipe_device, install_udev_rules, ) @@ -88,6 +89,9 @@ def send_pin_handler(args, client): def install_udev_rules_handler(args): return install_udev_rules('udev', args.location) +def update_firmware_handler(args, client): + return update_firmware(client, args.file) + class HWIHelpFormatter(argparse.ArgumentDefaultsHelpFormatter, argparse.RawDescriptionHelpFormatter): pass @@ -208,6 +212,10 @@ def process_commands(cli_args): sendpin_parser.add_argument('pin', help='The numeric positions of the PIN') sendpin_parser.set_defaults(func=send_pin_handler) + update_firmware_parser = subparsers.add_parser('updatefirmware', help='Verify and load firmware from a file onto a device') + update_firmware_parser.add_argument('file', help='The path to the firmware file') + update_firmware_parser.set_defaults(func=update_firmware_handler) + if sys.platform.startswith("linux"): udevrules_parser = subparsers.add_parser('installudevrules', help='Install and load the udev rule files for the hardware wallet devices') udevrules_parser.add_argument('--location', help='The path where the udev rules files will be copied', default='/etc/udev/rules.d/') diff --git a/hwilib/commands.py b/hwilib/commands.py index 54b41150f..4c00440a2 100644 --- a/hwilib/commands.py +++ b/hwilib/commands.py @@ -329,3 +329,6 @@ def install_udev_rules(source, location): from .udevinstaller import UDevInstaller return UDevInstaller.install(source, location) return {'error': 'udev rules are not needed on your platform', 'code': NOT_IMPLEMENTED} + +def update_firmware(client, file): + return client.update_firmware(file) diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py index 382f52832..40060b58b 100644 --- a/hwilib/devices/bitbox02.py +++ b/hwilib/devices/bitbox02.py @@ -627,3 +627,7 @@ def restore_device( bb02.set_device_name(label) return {"success": bb02.restore_from_mnemonic()} + + # Verify firmware file then load it onto device + def update_firmware(self, filename: str) -> Dict[str, bool]: + raise NotImplementedYet("The BitBox02 does not implement this method yet") diff --git a/hwilib/devices/ckcc/sigheader.py b/hwilib/devices/ckcc/sigheader.py index a39ac430a..e0df5e65f 100644 --- a/hwilib/devices/ckcc/sigheader.py +++ b/hwilib/devices/ckcc/sigheader.py @@ -30,9 +30,9 @@ FW_MAX_LENGTH = (0x100000 - 0x8000) # Arguments to be used w/ python's struct module. -FWH_PY_FORMAT = " Dict[str, bool]: + with open(filename, 'rb') as fd: + # learn size (portable way) + offset = 0 + sz = fd.seek(0, 2) + fd.seek(0) + + # Unwrap DFU contents, if needed. Also handles raw binary file. + try: + if fd.read(5) == b'DfuSe': + # expecting a DFU-wrapped file. + fd.seek(0) + offset, sz, *_ = dfu_parse(fd) + else: + # assume raw binary + pass + + assert sz % 256 == 0, "un-aligned size: %s" % sz + fd.seek(offset + FW_HEADER_OFFSET) + hdr = fd.read(FW_HEADER_SIZE) + + magic = struct.unpack_from(" Dict[str, bool]: + if self.device.get_product_string() != 'bootloader': + print('Device is not in bootloader mode. Unlocking bootloader, replugging will be required', file=sys.stderr) + print("Touch the device for 3 seconds to unlock bootloaderr. Touch briefly to cancel", file=sys.stderr) + reply = send_encrypt('{"bootloader":"unlock"}', self.password, self.device) + if 'error' in reply: + raise DBBError(reply) + 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} + + with open(filename, "rb") as f: + data = bytearray() + while True: + d = f.read(chunksize) + if len(d) == 0: + break + data = data + bytearray(d) + data = data + b'\xFF' * (applen - len(data)) + firmware = data[448:] + sig = data[:448] + verify_firmware(sig, firmware) + + sendPlainBoot("b", self.device) # blink led + sendPlainBoot("v", self.device) # bootloader version + sendPlainBoot("e", self.device) # erase existing firmware (required) + + # Send firmware + f = io.BytesIO(firmware) + cnt = 0 + while True: + chunk = f.read(chunksize) + if len(chunk) == 0: + break + sendChunk(cnt, chunk, self.device) + cnt += 1 + + # upload sigs and verify new firmware + load_result = sendPlainBoot("s" + "0" + binascii.hexlify(sig).decode(), self.device) + if load_result[1] == 'V': + latest_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64:][:8])) + app_version, = struct.unpack('>I', binascii.unhexlify(load_result[2 + 64 + 8:][:8])) + return {'error': 'firmware downgrade not allowed. Got version %d, but must be equal or higher to %d' % (app_version, latest_version), 'code': BAD_ARGUMENT} + elif load_result[1] != '0': + return {'error': 'invalid firmware signature', 'code': BAD_ARGUMENT} + + print('Please unplug and replug your device. The bootloader will be locked next time you use HWI with it.', file=sys.stderr) + return {'success': True} + def enumerate(password=''): results = [] devices = hid.enumerate(DBB_VENDOR_ID, DBB_DEVICE_ID) diff --git a/hwilib/devices/ledger.py b/hwilib/devices/ledger.py index 3880ccee9..d694c6f5a 100644 --- a/hwilib/devices/ledger.py +++ b/hwilib/devices/ledger.py @@ -386,6 +386,10 @@ def send_pin(self, pin): def toggle_passphrase(self): raise UnavailableActionError('The Ledger Nano S and X do not support toggling passphrase from the host') + # Verify firmware file then load it onto device + def update_firmware(self, filename: str) -> Dict[str, bool]: + raise UnavailableActionError('The Ledger Nano S and X do not support firmware updates from 3rd party software.') + def enumerate(password=''): results = [] devices = [] diff --git a/hwilib/devices/trezor.py b/hwilib/devices/trezor.py index 6c73aab57..dd1af4e28 100644 --- a/hwilib/devices/trezor.py +++ b/hwilib/devices/trezor.py @@ -15,6 +15,7 @@ common_err_msgs, handle_errors, ) +from .trezorlib import firmware from .trezorlib.client import TrezorClient as Trezor from .trezorlib.debuglink import TrezorClientDebugLink from .trezorlib.exceptions import Cancelled @@ -131,6 +132,42 @@ def interactive_get_pin(self, code=None): else: return pin +ALLOWED_FIRMWARE_FORMATS = { + 1: (firmware.FirmwareFormat.TREZOR_ONE, firmware.FirmwareFormat.TREZOR_ONE_V2, firmware.FirmwareFormat.KEEPKEY), + 2: (firmware.FirmwareFormat.TREZOR_T, firmware.FirmwareFormat.KEEPKEY), +} + +def _print_version(version): + vstr = "Firmware version {major}.{minor}.{patch} build {build}".format(**version) + print(vstr, file=sys.stderr) + +def validate_firmware(version, fw, expected_fingerprint=None): + if version == firmware.FirmwareFormat.TREZOR_ONE: + if fw.embedded_onev2: + print("Trezor One firmware with embedded v2 image (1.8.0 or later)", file=sys.stderr) + _print_version(fw.embedded_onev2.firmware_header.version) + else: + print("Trezor One firmware image.", file=sys.stderr) + elif version == firmware.FirmwareFormat.TREZOR_ONE_V2: + print('Keepkey firmware image', file=sys.stderr) + elif version == firmware.FirmwareFormat.TREZOR_ONE_V2: + print("Trezor One v2 firmware (1.8.0 or later)", file=sys.stderr) + _print_version(fw.firmware_header.version) + elif version == firmware.FirmwareFormat.TREZOR_T: + print("Trezor T firmware image.", file=sys.stderr) + vendor = fw.vendor_header.vendor_string + vendor_version = "{major}.{minor}".format(**fw.vendor_header.version) + print("Vendor header from {}, version {}".format(vendor, vendor_version), file=sys.stderr) + _print_version(fw.firmware_header.version) + + firmware.validate(version, fw, allow_unsigned=False) + print("Signatures are valid.", file=sys.stderr) + + fingerprint = firmware.digest(version, fw).hex() + print("Firmware fingerprint: {}".format(fingerprint), file=sys.stderr) + if expected_fingerprint and fingerprint != expected_fingerprint: + raise BadArgumentError('Expected firmware fingerprint {} does not match computed fingerprint {}.'.format(expected_fingerprint, fingerprint)) + # This class extends the HardwareWalletClient for Trezor specific things class TrezorClient(HardwareWalletClient): @@ -551,6 +588,41 @@ def toggle_passphrase(self): print(PIN_MATRIX_DESCRIPTION, file=sys.stderr) return {'success': True} + # Verify firmware file then load it onto device + @trezor_exception + def update_firmware(self, filename: str) -> Dict[str, bool]: + self.client.init_device(True) + if not self.client.features.bootloader_mode: + raise DeviceConnectionError('Device needs to be in bootloader mode') + + bootloader_onev2 = self.client.features.major_version == 1 and self.client.features.minor_version >= 8 + + data = open(filename, "rb").read() + version, fw = firmware.parse(data) + validate_firmware(version, fw) + + if bootloader_onev2 and version == firmware.FirmwareFormat.TREZOR_ONE and not fw.embedded_onev2: + raise BadArgumentError('Firmware is too old for your device') + elif not bootloader_onev2 and version == firmware.FirmwareFormat.TREZOR_ONE_V2: + raise BadArgumentError('You need to upgrade to bootloader 1.8.0 first.') + + if self.client.features.major_version not in ALLOWED_FIRMWARE_FORMATS: + raise BadArgumentError('Device has unknown version, unable to upgrade firmware') + elif version not in ALLOWED_FIRMWARE_FORMATS[self.client.features.major_version]: + raise BadArgumentError('Firmware does not match your device') + + # special handling for embedded-OneV2 format: + # for bootloader < 1.8, keep the embedding + # for bootloader 1.8.0 and up, strip the old OneV1 header + if bootloader_onev2 and data[:4] == b"TRZR" and data[256:256 + 4] == b"TRZF": + data = data[256:] + + if self.client.features.major_version == 1 and self.client.features.firmware_present is not False: + # Trezor One does not send ButtonRequest + print("Please confirm the action on your device", file=sys.stderr) + firmware.update(self.client, data, version) + return {'success': True} + def enumerate(password=''): results = [] for dev in enumerate_devices(): @@ -566,7 +638,7 @@ def enumerate(password=''): client = None with handle_errors(common_err_msgs["enumerate"], d_data): client = TrezorClient(d_data['path'], password) - client.client.init_device() + client.client.init_device(True) if 'trezor' not in client.client.features.vendor: continue diff --git a/hwilib/devices/trezorlib/_ed25519.py b/hwilib/devices/trezorlib/_ed25519.py new file mode 100644 index 000000000..f1959ac96 --- /dev/null +++ b/hwilib/devices/trezorlib/_ed25519.py @@ -0,0 +1,299 @@ +# ed25519.py - Optimized version of the reference implementation of Ed25519 +# downloaded from https://github.com/pyca/ed25519 +# +# Written in 2011? by Daniel J. Bernstein +# 2013 by Donald Stufft +# 2013 by Alex Gaynor +# 2013 by Greg Price +# +# To the extent possible under law, the author(s) have dedicated all copyright +# and related and neighboring rights to this software to the public domain +# worldwide. This software is distributed without any warranty. +# +# You should have received a copy of the CC0 Public Domain Dedication along +# with this software. If not, see +# . + +""" +NB: This code is not safe for use with secret keys or secret data. +The only safe use of this code is for verifying signatures on public messages. + +Functions for computing the public key of a secret key and for signing +a message are included, namely publickey_unsafe and signature_unsafe, +for testing purposes only. + +The root of the problem is that Python's long-integer arithmetic is +not designed for use in cryptography. Specifically, it may take more +or less time to execute an operation depending on the values of the +inputs, and its memory access patterns may also depend on the inputs. +This opens it to timing and cache side-channel attacks which can +disclose data to an attacker. We rely on Python's long-integer +arithmetic, so we cannot handle secrets without risking their disclosure. +""" + +import hashlib +from typing import List, NewType, Tuple + +Point = NewType("Point", Tuple[int, int, int, int]) + + +__version__ = "1.0.dev1" + + +b = 256 +q = 2 ** 255 - 19 +l = 2 ** 252 + 27742317777372353535851937790883648493 + +COORD_MASK = ~(1 + 2 + 4 + (1 << b - 1)) +COORD_HIGH_BIT = 1 << b - 2 + + +def H(m: bytes) -> bytes: + return hashlib.sha512(m).digest() + + +def pow2(x: int, p: int) -> int: + """== pow(x, 2**p, q)""" + while p > 0: + x = x * x % q + p -= 1 + return x + + +def inv(z: int) -> int: + """$= z^{-1} mod q$, for z != 0""" + # Adapted from curve25519_athlon.c in djb's Curve25519. + z2 = z * z % q # 2 + z9 = pow2(z2, 2) * z % q # 9 + z11 = z9 * z2 % q # 11 + z2_5_0 = (z11 * z11) % q * z9 % q # 31 == 2^5 - 2^0 + z2_10_0 = pow2(z2_5_0, 5) * z2_5_0 % q # 2^10 - 2^0 + z2_20_0 = pow2(z2_10_0, 10) * z2_10_0 % q # ... + z2_40_0 = pow2(z2_20_0, 20) * z2_20_0 % q + z2_50_0 = pow2(z2_40_0, 10) * z2_10_0 % q + z2_100_0 = pow2(z2_50_0, 50) * z2_50_0 % q + z2_200_0 = pow2(z2_100_0, 100) * z2_100_0 % q + z2_250_0 = pow2(z2_200_0, 50) * z2_50_0 % q # 2^250 - 2^0 + return pow2(z2_250_0, 5) * z11 % q # 2^255 - 2^5 + 11 = q - 2 + + +d = -121665 * inv(121666) % q +I = pow(2, (q - 1) // 4, q) + + +def xrecover(y: int) -> int: + xx = (y * y - 1) * inv(d * y * y + 1) + x = pow(xx, (q + 3) // 8, q) + + if (x * x - xx) % q != 0: + x = (x * I) % q + + if x % 2 != 0: + x = q - x + + return x + + +By = 4 * inv(5) +Bx = xrecover(By) +B = Point((Bx % q, By % q, 1, (Bx * By) % q)) +ident = Point((0, 1, 1, 0)) + + +def edwards_add(P: Point, Q: Point) -> Point: + # This is formula sequence 'addition-add-2008-hwcd-3' from + # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html + (x1, y1, z1, t1) = P + (x2, y2, z2, t2) = Q + + a = (y1 - x1) * (y2 - x2) % q + b = (y1 + x1) * (y2 + x2) % q + c = t1 * 2 * d * t2 % q + dd = z1 * 2 * z2 % q + e = b - a + f = dd - c + g = dd + c + h = b + a + x3 = e * f + y3 = g * h + t3 = e * h + z3 = f * g + + return Point((x3 % q, y3 % q, z3 % q, t3 % q)) + + +def edwards_double(P: Point) -> Point: + # This is formula sequence 'dbl-2008-hwcd' from + # http://www.hyperelliptic.org/EFD/g1p/auto-twisted-extended-1.html + (x1, y1, z1, _) = P + + a = x1 * x1 % q + b = y1 * y1 % q + c = 2 * z1 * z1 % q + # dd = -a + e = ((x1 + y1) * (x1 + y1) - a - b) % q + g = -a + b # dd + b + f = g - c + h = -a - b # dd - b + x3 = e * f + y3 = g * h + t3 = e * h + z3 = f * g + + return Point((x3 % q, y3 % q, z3 % q, t3 % q)) + + +def scalarmult(P: Point, e: int) -> Point: + if e == 0: + return ident + Q = scalarmult(P, e // 2) + Q = edwards_double(Q) + if e & 1: + Q = edwards_add(Q, P) + return Q + + +# Bpow[i] == scalarmult(B, 2**i) +Bpow = [] # type: List[Point] + + +def make_Bpow() -> None: + P = B + for _ in range(253): + Bpow.append(P) + P = edwards_double(P) + + +make_Bpow() + + +def scalarmult_B(e: int) -> Point: + """ + Implements scalarmult(B, e) more efficiently. + """ + # scalarmult(B, l) is the identity + e = e % l + P = ident + for i in range(253): + if e & 1: + P = edwards_add(P, Bpow[i]) + e = e // 2 + assert e == 0, e + return P + + +def encodeint(y: int) -> bytes: + return y.to_bytes(b // 8, "little") + + +def encodepoint(P: Point) -> bytes: + (x, y, z, _) = P + zi = inv(z) + x = (x * zi) % q + y = (y * zi) % q + + xbit = (x & 1) << (b - 1) + y_result = y & ~xbit # clear x bit + y_result |= xbit # set corret x bit value + return encodeint(y_result) + + +def decodeint(s: bytes) -> int: + return int.from_bytes(s, "little") + + +def decodepoint(s: bytes) -> Point: + y = decodeint(s) & ~(1 << b - 1) # y without the highest bit + x = xrecover(y) + if x & 1 != bit(s, b - 1): + x = q - x + P = Point((x, y, 1, (x * y) % q)) + if not isoncurve(P): + raise ValueError("decoding point that is not on curve") + return P + + +def decodecoord(s: bytes) -> int: + a = decodeint(s[: b // 8]) + # clear mask bits + a &= COORD_MASK + # set high bit + a |= COORD_HIGH_BIT + return a + + +def bit(h: bytes, i: int) -> int: + return (h[i // 8] >> (i % 8)) & 1 + + +def publickey_unsafe(sk: bytes) -> bytes: + """ + Not safe to use with secret keys or secret data. + + See module docstring. This function should be used for testing only. + """ + h = H(sk) + a = decodecoord(h) + A = scalarmult_B(a) + return encodepoint(A) + + +def Hint(m: bytes) -> int: + return decodeint(H(m)) + + +def signature_unsafe(m: bytes, sk: bytes, pk: bytes) -> bytes: + """ + Not safe to use with secret keys or secret data. + + See module docstring. This function should be used for testing only. + """ + h = H(sk) + a = decodecoord(h) + r = Hint(h[b // 8 : b // 4] + m) + R = scalarmult_B(r) + S = (r + Hint(encodepoint(R) + pk + m) * a) % l + return encodepoint(R) + encodeint(S) + + +def isoncurve(P: Point) -> bool: + (x, y, z, t) = P + return ( + z % q != 0 + and x * y % q == z * t % q + and (y * y - x * x - z * z - d * t * t) % q == 0 + ) + + +class SignatureMismatch(Exception): + pass + + +def checkvalid(s: bytes, m: bytes, pk: bytes) -> None: + """ + Not safe to use when any argument is secret. + + See module docstring. This function should be used only for + verifying public signatures of public messages. + """ + if len(s) != b // 4: + raise ValueError("signature length is wrong") + + if len(pk) != b // 8: + raise ValueError("public-key length is wrong") + + R = decodepoint(s[: b // 8]) + A = decodepoint(pk) + S = decodeint(s[b // 8 : b // 4]) + h = Hint(encodepoint(R) + pk + m) + + (x1, y1, z1, _) = P = scalarmult_B(S) + (x2, y2, z2, _) = Q = edwards_add(R, scalarmult(A, h)) + + if ( + not isoncurve(P) + or not isoncurve(Q) + or (x1 * z2 - x2 * z1) % q != 0 + or (y1 * z2 - y2 * z1) % q != 0 + ): + raise SignatureMismatch("signature does not pass verification") diff --git a/hwilib/devices/trezorlib/client.py b/hwilib/devices/trezorlib/client.py index a802c32f2..f774a6a4a 100644 --- a/hwilib/devices/trezorlib/client.py +++ b/hwilib/devices/trezorlib/client.py @@ -187,8 +187,10 @@ def call(self, msg): return resp @tools.session - def init_device(self): - resp = self.call_raw(messages.GetFeatures()) + def init_device(self, initialize=False): + resp = messages.Failure() + if not initialize: + resp = self.call_raw(messages.GetFeatures()) # If GetFeatures fails, try initializing and clearing inconsistent state on the device if isinstance(resp, messages.Failure): resp = self.call_raw(messages.Initialize()) diff --git a/hwilib/devices/trezorlib/cosi.py b/hwilib/devices/trezorlib/cosi.py new file mode 100644 index 000000000..8d87e41f0 --- /dev/null +++ b/hwilib/devices/trezorlib/cosi.py @@ -0,0 +1,101 @@ +# This file is part of the Trezor project. +# +# Copyright (C) 2012-2019 SatoshiLabs and contributors +# +# This library is free software: you can redistribute it and/or modify +# it under the terms of the GNU Lesser General Public License version 3 +# as published by the Free Software Foundation. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Lesser General Public License for more details. +# +# You should have received a copy of the License along with this library. +# If not, see . + +from functools import reduce +from typing import Iterable, List, Tuple + +from . import _ed25519, messages +from .tools import expect + +# XXX, these could be NewType's, but that would infect users of the cosi module with these types as well. +# Unsure if we want that. +Ed25519PrivateKey = bytes +Ed25519PublicPoint = bytes +Ed25519Signature = bytes + + +def combine_keys(pks: Iterable[Ed25519PublicPoint]) -> Ed25519PublicPoint: + """Combine a list of Ed25519 points into a "global" CoSi key.""" + P = [_ed25519.decodepoint(pk) for pk in pks] + combine = reduce(_ed25519.edwards_add, P) + return Ed25519PublicPoint(_ed25519.encodepoint(combine)) + + +def combine_sig( + global_R: Ed25519PublicPoint, sigs: Iterable[Ed25519Signature] +) -> Ed25519Signature: + """Combine a list of signatures into a single CoSi signature.""" + S = [_ed25519.decodeint(si) for si in sigs] + s = sum(S) % _ed25519.l + sig = global_R + _ed25519.encodeint(s) + return Ed25519Signature(sig) + + +def get_nonce( + sk: Ed25519PrivateKey, data: bytes, ctr: int = 0 +) -> Tuple[int, Ed25519PublicPoint]: + """Calculate CoSi nonces for given data. + These differ from Ed25519 deterministic nonces in that there is a counter appended at end. + + Returns both the private point `r` and the partial signature `R`. + `r` is returned for performance reasons: :func:`sign_with_privkey` + takes it as its `nonce` argument so that it doesn't repeat the `get_nonce` call. + + `R` should be combined with other partial signatures through :func:`combine_keys` + to obtain a "global commitment". + """ + # r = hash(hash(sk)[b .. 2b] + M + ctr) + # R = rB + h = _ed25519.H(sk) + bytesize = _ed25519.b // 8 + assert len(h) == bytesize * 2 + r = _ed25519.Hint(h[bytesize:] + data + ctr.to_bytes(4, "big")) + R = _ed25519.scalarmult(_ed25519.B, r) + return r, Ed25519PublicPoint(_ed25519.encodepoint(R)) + + +def verify( + signature: Ed25519Signature, digest: bytes, pub_key: Ed25519PublicPoint +) -> None: + """Verify Ed25519 signature. Raise exception if the signature is invalid.""" + # XXX this *might* change to bool function + _ed25519.checkvalid(signature, digest, pub_key) + + +def verify_m_of_n( + signature: Ed25519Signature, + digest: bytes, + m: int, + n: int, + mask: int, + keys: List[Ed25519PublicPoint], +) -> None: + if m < 1: + raise ValueError("At least 1 signer must be specified") + selected_keys = [keys[i] for i in range(n) if mask & (1 << i)] + if len(selected_keys) < m: + raise ValueError( + "Not enough signers ({} required, {} found)".format(m, len(selected_keys)) + ) + global_pk = combine_keys(selected_keys) + return verify(signature, digest, global_pk) + + +def pubkey_from_privkey(privkey: Ed25519PrivateKey) -> Ed25519PublicPoint: + """Interpret 32 bytes of data as an Ed25519 private key. + Calculate and return the corresponding public key. + """ + return Ed25519PublicPoint(_ed25519.publickey_unsafe(privkey)) diff --git a/hwilib/devices/trezorlib/firmware.py b/hwilib/devices/trezorlib/firmware.py index e80a5af55..025686626 100644 --- a/hwilib/devices/trezorlib/firmware.py +++ b/hwilib/devices/trezorlib/firmware.py @@ -1,6 +1,6 @@ # This file is part of the Trezor project. # -# Copyright (C) 2012-2018 SatoshiLabs and contributors +# Copyright (C) 2012-2019 SatoshiLabs and contributors # # This library is free software: you can redistribute it and/or modify # it under the terms of the GNU Lesser General Public License version 3 @@ -16,14 +16,19 @@ import hashlib from enum import Enum -from typing import NewType, Tuple +from typing import Callable, List, NewType, Tuple import construct as c import ecdsa -import pyblake2 from . import cosi, messages, tools +try: + from hashlib import blake2s +except ImportError: + from pyblake2 import blake2s + + V1_SIGNATURE_SLOTS = 3 V1_BOOTLOADER_KEYS = { 1: "04d571b7f148c5e4232c3814f777d8faeaf1a84216c78d569b71041ffc768a5b2d810fc3bb134dd026b57e65005275aedef43e155f48fc11a32ec790a93312bd58", @@ -32,6 +37,13 @@ 4: "04877c39fd7c62237e038235e9c075dab261630f78eeb8edb92487159fffedfdf6046c6f8b881fa407c4a4ce6c28de0b19c1f4e29f1fcbc5a58ffd1432a3e0938a", 5: "047384c51ae81add0a523adbb186c91b906ffb64c2c765802bf26dbd13bdf12c319e80c2213a136c8ee03d7874fd22b70d68e7dee469decfbbb510ee9a460cda45", } +KEEPKEY_BOOTLOADER_KEYS = { + 1: "04a33cec36d6d011af09e0c498d17c3ba7ab907abfbb64caba16ad9077caacd3e198a32362c32d0ef0a7269259abbbcd8a688a0c8f54a6dbc405459566cd65141d", + 2: "04ab291f6bd33d0e3974f27e50070be933695a0fab7b8b3654e7c9dce74f7f98fd739b1ed86eb0be26f026e4dc6519fd2884955fa174f8a783fe455ac43f944c70", + 3: "04a9c29f4e053b35ffd3b9a37b073188b624c07c3a92622c131edf1a2b2c712216a8c06c9ddfdcaa39b81d9a86f0459480b0277eab0e30a34f1d26b326b8995a33", + 4: "04f228448eaf05171ccb68a04a0724ac586b846c54c5fd0a526f9d7c3396c98dd47aef6b2faf47b54ffa8c2861c54920ce6c2aa5607c496869023724db285495c6", + 5: "0418a90b536e9ffb0ec320293c33754af89b145475c4d921f818e2062c92b01be526047ccfa042b4711fb5603fe6bd7980693100b71ee766d86116a3694873f314", +} V2_BOOTLOADER_KEYS = [ bytes.fromhex("c2c87a49c5a3460977fbb2ec9dfe60f06bd694db8244bd4981fe3b7a26307f3f"), @@ -41,6 +53,7 @@ V2_BOOTLOADER_M = 2 V2_BOOTLOADER_N = 3 +ONEV2_CHUNK_SIZE = 1024 * 64 V2_CHUNK_SIZE = 1024 * 128 @@ -57,10 +70,42 @@ def _transform_vendor_trust(data: bytes) -> bytes: return bytes(~b & 0xFF for b in data)[::-1] +class FirmwareIntegrityError(Exception): + pass + + +class InvalidSignatureError(FirmwareIntegrityError): + pass + + +class Unsigned(FirmwareIntegrityError): + pass + + +class ToifMode(Enum): + full_color = b"f" + grayscale = b"g" + + +class EnumAdapter(c.Adapter): + def __init__(self, subcon, enum): + self.enum = enum + super().__init__(subcon) + + def _encode(self, obj, ctx, path): + return obj.value + + def _decode(self, obj, ctx, path): + try: + return self.enum(obj) + except ValueError: + return obj + + # fmt: off Toif = c.Struct( "magic" / c.Const(b"TOI"), - "format" / c.Enum(c.Byte, full_color=b"f", grayscale=b"g"), + "format" / EnumAdapter(c.Bytes(1), ToifMode), "width" / c.Int16ul, "height" / c.Int16ul, "data" / c.Prefixed(c.Int32ul, c.GreedyBytes), @@ -117,7 +162,7 @@ def _transform_vendor_trust(data: bytes) -> bytes: FirmwareHeader = c.Struct( "_start_offset" / c.Tell, "magic" / c.Const(b"TRZF"), - "_header_len" / c.Padding(4), + "header_len" / c.Int32ul, "expiry" / c.Int32ul, "code_length" / c.Rebuild( c.Int32ul, @@ -130,14 +175,21 @@ def _transform_vendor_trust(data: bytes) -> bytes: "reserved" / c.Padding(8), "hashes" / c.Bytes(32)[16], - "reserved" / c.Padding(415), + "v1_signatures" / c.Bytes(64)[V1_SIGNATURE_SLOTS], + "v1_key_indexes" / c.Int8ul[V1_SIGNATURE_SLOTS], # pylint: disable=E1136 + + "reserved" / c.Padding(220), "sigmask" / c.Byte, "signature" / c.Bytes(64), "_end_offset" / c.Tell, - "header_len" / c.Pointer( - c.this._start_offset + 4, - c.Rebuild(c.Int32ul, c.this._end_offset - c.this._start_offset) + + "_rebuild_header_len" / c.If( + c.this.version.major > 1, + c.Pointer( + c.this._start_offset + 4, + c.Rebuild(c.Int32ul, c.this._end_offset - c.this._start_offset) + ), ), ) @@ -151,7 +203,15 @@ def _transform_vendor_trust(data: bytes) -> bytes: ) -FirmwareV1 = c.Struct( +FirmwareOneV2 = c.Struct( + "firmware_header" / FirmwareHeader, + "_code_offset" / c.Tell, + "code" / c.Bytes(c.this.firmware_header.code_length), + c.Terminated, +) + + +FirmwareOne = c.Struct( "magic" / c.Const(b"TRZR"), "code_length" / c.Rebuild(c.Int32ul, c.len_(c.this.code)), "key_indexes" / c.Int8ul[V1_SIGNATURE_SLOTS], # pylint: disable=E1136 @@ -163,6 +223,23 @@ def _transform_vendor_trust(data: bytes) -> bytes: "signatures" / c.Bytes(64)[V1_SIGNATURE_SLOTS], "code" / c.Bytes(c.this.code_length), c.Terminated, + + "embedded_onev2" / c.RestreamData(c.this.code, c.Optional(FirmwareOneV2)), +) + + +FirmwareKeepkey = c.Struct( + "magic" / c.Const(b"KPKY"), + "code_length" / c.Rebuild(c.Int32ul, c.len_(c.this.code)), + "key_indexes" / c.Int8ul[V1_SIGNATURE_SLOTS], # pylint: disable=E1136 + "flags" / c.BitStruct( + c.Padding(7), + "restore_storage" / c.Flag, + ), + "reserved" / c.Padding(52), + "signatures" / c.Bytes(64)[V1_SIGNATURE_SLOTS], + "code" / c.Bytes(c.this.code_length), + c.Terminated, ) # fmt: on @@ -171,6 +248,8 @@ def _transform_vendor_trust(data: bytes) -> bytes: class FirmwareFormat(Enum): TREZOR_ONE = 1 TREZOR_T = 2 + TREZOR_ONE_V2 = 3 + KEEPKEY = 4 FirmwareType = NewType("FirmwareType", c.Container) @@ -180,62 +259,148 @@ class FirmwareFormat(Enum): def parse(data: bytes) -> ParsedFirmware: if data[:4] == b"TRZR": version = FirmwareFormat.TREZOR_ONE - cls = FirmwareV1 + cls = FirmwareOne elif data[:4] == b"TRZV": version = FirmwareFormat.TREZOR_T cls = Firmware + elif data[:4] == b"TRZF": + version = FirmwareFormat.TREZOR_ONE_V2 + cls = FirmwareOneV2 + elif data[:4] == b'KPKY': + version = FirmwareFormat.KEEPKEY + cls = FirmwareKeepkey else: raise ValueError("Unrecognized firmware image type") try: fw = cls.parse(data) except Exception as e: - raise ValueError("Invalid firmware image") from e + raise FirmwareIntegrityError("Invalid firmware image") from e return version, FirmwareType(fw) -def digest_v1(fw: FirmwareType) -> bytes: +def digest_onev1(fw: FirmwareType) -> bytes: return hashlib.sha256(fw.code).digest() -def check_sig_v1(fw: FirmwareType, idx: int) -> bool: - key_idx = fw.key_indexes[idx] - signature = fw.signatures[idx] +def check_sig_v1( + digest: bytes, key_indexes: List[int], signatures: List[bytes], is_keepkey: bool = False +) -> None: + distinct_key_indexes = set(i for i in key_indexes if i != 0) + if not distinct_key_indexes: + raise Unsigned - if key_idx == 0: - # no signature = invalid signature - return False + if len(distinct_key_indexes) < len(key_indexes): + raise InvalidSignatureError( + "Not enough distinct signatures (found {}, need {})".format( + len(distinct_key_indexes), len(key_indexes) + ) + ) - if key_idx not in V1_BOOTLOADER_KEYS: - # unknown pubkey - return False + bootloader_keys = KEEPKEY_BOOTLOADER_KEYS if is_keepkey else V1_BOOTLOADER_KEYS - pubkey = bytes.fromhex(V1_BOOTLOADER_KEYS[key_idx])[1:] - verify = ecdsa.VerifyingKey.from_string( - pubkey, curve=ecdsa.curves.SECP256k1, hashfunc=hashlib.sha256 - ) - try: - verify.verify(signature, fw.code) - return True - except ecdsa.BadSignatureError: - return False + for i in range(len(key_indexes)): + key_idx = key_indexes[i] + signature = signatures[i] + + if key_idx not in bootloader_keys: + # unknown pubkey + raise InvalidSignatureError("Unknown key in slot {}".format(i)) + + pubkey = bytes.fromhex(bootloader_keys[key_idx])[1:] + verify = ecdsa.VerifyingKey.from_string(pubkey, curve=ecdsa.curves.SECP256k1) + try: + verify.verify_digest(signature, digest) + except ecdsa.BadSignatureError as e: + raise InvalidSignatureError("Invalid signature in slot {}".format(i)) from e -def _header_digest(header: c.Container, header_type: c.Construct) -> bytes: +def _header_digest( + header: c.Container, header_type: c.Construct, hash_function: Callable = blake2s +) -> bytes: stripped_header = header.copy() stripped_header.sigmask = 0 stripped_header.signature = b"\0" * 64 + stripped_header.v1_key_indexes = [0, 0, 0] + stripped_header.v1_signatures = [b"\0" * 64] * 3 header_bytes = header_type.build(stripped_header) - return pyblake2.blake2s(header_bytes).digest() + return hash_function(header_bytes).digest() + + +def digest_v2(fw: FirmwareType) -> bytes: + return _header_digest(fw.firmware_header, FirmwareHeader, blake2s) + + +def digest_onev2(fw: FirmwareType) -> bytes: + return _header_digest(fw.firmware_header, FirmwareHeader, hashlib.sha256) + +def validate_code_hashes( + fw: FirmwareType, + hash_function: Callable = blake2s, + chunk_size: int = V2_CHUNK_SIZE, + padding_byte: bytes = None, +) -> None: + for i, expected_hash in enumerate(fw.firmware_header.hashes): + if i == 0: + # Because first chunk is sent along with headers, there is less code in it. + chunk = fw.code[: chunk_size - fw._code_offset] + else: + # Subsequent chunks are shifted by the "missing header" size. + ptr = i * chunk_size - fw._code_offset + chunk = fw.code[ptr : ptr + chunk_size] -def digest(fw: FirmwareType) -> bytes: - return _header_digest(fw.firmware_header, FirmwareHeader) + # padding for last chunk + if padding_byte is not None and i > 1 and chunk and len(chunk) < chunk_size: + chunk += padding_byte[0:1] * (chunk_size - len(chunk)) + + if not chunk and expected_hash == b"\0" * 32: + continue + chunk_hash = hash_function(chunk).digest() + if chunk_hash != expected_hash: + raise FirmwareIntegrityError("Invalid firmware data.") -def validate(fw: FirmwareType, skip_vendor_header=False) -> bool: +def validate_onev2(fw: FirmwareType, allow_unsigned: bool = False) -> None: + try: + check_sig_v1( + digest_onev2(fw), + fw.firmware_header.v1_key_indexes, + fw.firmware_header.v1_signatures, + ) + except Unsigned: + if not allow_unsigned: + raise + + validate_code_hashes( + fw, + hash_function=hashlib.sha256, + chunk_size=ONEV2_CHUNK_SIZE, + padding_byte=b"\xFF", + ) + + +def validate_onev1(fw: FirmwareType, allow_unsigned: bool = False) -> None: + try: + check_sig_v1(digest_onev1(fw), fw.key_indexes, fw.signatures) + except Unsigned: + if not allow_unsigned: + raise + if fw.embedded_onev2: + validate_onev2(fw.embedded_onev2, allow_unsigned) + + +def validate_keepkey(fw: FirmwareType, allow_unsigned: bool = False) -> None: + try: + check_sig_v1(digest_onev1(fw), fw.key_indexes, fw.signatures, True) + except Unsigned: + if not allow_unsigned: + raise + + +def validate_v2(fw: FirmwareType, skip_vendor_header: bool = False) -> None: vendor_fingerprint = _header_digest(fw.vendor_header, VendorHeader) - fingerprint = digest(fw) + fingerprint = digest_v2(fw) if not skip_vendor_header: try: @@ -250,7 +415,7 @@ def validate(fw: FirmwareType, skip_vendor_header=False) -> bool: V2_BOOTLOADER_KEYS, ) except Exception: - raise ValueError("Invalid vendor header signature.") + raise InvalidSignatureError("Invalid vendor header signature.") # XXX expiry is not used now # now = time.gmtime() @@ -267,35 +432,45 @@ def validate(fw: FirmwareType, skip_vendor_header=False) -> bool: fw.vendor_header.pubkeys, ) except Exception: - raise ValueError("Invalid firmware signature.") + raise InvalidSignatureError("Invalid firmware signature.") # XXX expiry is not used now # if time.gmtime(fw.firmware_header.expiry) < now: # raise ValueError("Firmware header expired.") + validate_code_hashes(fw) - for i, expected_hash in enumerate(fw.firmware_header.hashes): - if i == 0: - # Because first chunk is sent along with headers, there is less code in it. - chunk = fw.code[: V2_CHUNK_SIZE - fw._code_offset] - else: - # Subsequent chunks are shifted by the "missing header" size. - ptr = i * V2_CHUNK_SIZE - fw._code_offset - chunk = fw.code[ptr : ptr + V2_CHUNK_SIZE] - - if not chunk and expected_hash == b"\0" * 32: - continue - chunk_hash = pyblake2.blake2s(chunk).digest() - if chunk_hash != expected_hash: - raise ValueError("Invalid firmware data.") - return True +def digest(version: FirmwareFormat, fw: FirmwareType) -> bytes: + if version == FirmwareFormat.TREZOR_ONE or version == FirmwareFormat.KEEPKEY: + return digest_onev1(fw) + elif version == FirmwareFormat.TREZOR_ONE_V2: + return digest_onev2(fw) + elif version == FirmwareFormat.TREZOR_T: + return digest_v2(fw) + else: + raise ValueError("Unrecognized firmware version") + + +def validate( + version: FirmwareFormat, fw: FirmwareType, allow_unsigned: bool = False +) -> None: + if version == FirmwareFormat.TREZOR_ONE: + return validate_onev1(fw, allow_unsigned) + elif version == FirmwareFormat.KEEPKEY: + return validate_keepkey(fw, allow_unsigned) + elif version == FirmwareFormat.TREZOR_ONE_V2: + return validate_onev2(fw, allow_unsigned) + elif version == FirmwareFormat.TREZOR_T: + return validate_v2(fw) + else: + raise ValueError("Unrecognized firmware version") # ====== Client functions ====== # @tools.session -def update(client, data): +def update(client, data, fw_version): if client.features.bootloader_mode is False: raise RuntimeError("Device must be in bootloader mode") @@ -303,7 +478,11 @@ def update(client, data): # TREZORv1 method if isinstance(resp, messages.Success): - resp = client.call(messages.FirmwareUpload(payload=data)) + if fw_version == FirmwareFormat.KEEPKEY: + data_hash = hashlib.sha256(data).digest() + resp = client.call(messages.FirmwareUploadKeepkey(payload=data, hash=data_hash)) + else: + resp = client.call(messages.FirmwareUpload(payload=data)) if isinstance(resp, messages.Success): return else: @@ -312,7 +491,7 @@ def update(client, data): # TREZORv2 method while isinstance(resp, messages.FirmwareRequest): payload = data[resp.offset : resp.offset + resp.length] - digest = pyblake2.blake2s(payload).digest() + digest = blake2s(payload).digest() resp = client.call(messages.FirmwareUpload(payload=payload, hash=digest)) if isinstance(resp, messages.Success): diff --git a/hwilib/devices/trezorlib/mapping.py b/hwilib/devices/trezorlib/mapping.py index 11c94cb06..a2e162d76 100644 --- a/hwilib/devices/trezorlib/mapping.py +++ b/hwilib/devices/trezorlib/mapping.py @@ -41,7 +41,8 @@ def build_map(): def register_message(msg_class): - if msg_class.MESSAGE_WIRE_TYPE in map_type_to_class: + # Ignore this error for FirmwareUploadKeepkey + if msg_class.MESSAGE_WIRE_TYPE in map_type_to_class and not messages.FirmwareUploadKeepkey: raise Exception( "Message for wire type %s is already registered by %s" % (msg_class.MESSAGE_WIRE_TYPE, get_class(msg_class.MESSAGE_WIRE_TYPE)) diff --git a/hwilib/devices/trezorlib/messages/FirmwareUpload.py b/hwilib/devices/trezorlib/messages/FirmwareUpload.py index 217273ed6..af80f7416 100644 --- a/hwilib/devices/trezorlib/messages/FirmwareUpload.py +++ b/hwilib/devices/trezorlib/messages/FirmwareUpload.py @@ -20,3 +20,21 @@ def get_fields(cls): 1: ('payload', p.BytesType, 0), # required 2: ('hash', p.BytesType, 0), } + +class FirmwareUploadKeepkey(p.MessageType): + MESSAGE_WIRE_TYPE = 7 + + def __init__( + self, + payload: bytes = None, + hash: bytes = None, + ) -> None: + self.payload = payload + self.hash = hash + + @classmethod + def get_fields(cls): + return { + 1: ('hash', p.BytesType, 0), + 2: ('payload', p.BytesType, 0), + } diff --git a/hwilib/devices/trezorlib/messages/MessageType.py b/hwilib/devices/trezorlib/messages/MessageType.py index d6ebc1743..fd49e40d8 100644 --- a/hwilib/devices/trezorlib/messages/MessageType.py +++ b/hwilib/devices/trezorlib/messages/MessageType.py @@ -32,6 +32,7 @@ GetFeatures = 55 FirmwareErase = 6 FirmwareUpload = 7 +FirmwareUploadKeepkey = 7 FirmwareRequest = 8 SelfTest = 32 GetPublicKey = 11 diff --git a/hwilib/devices/trezorlib/messages/__init__.py b/hwilib/devices/trezorlib/messages/__init__.py index 9a4ad93da..14dc7a9c3 100644 --- a/hwilib/devices/trezorlib/messages/__init__.py +++ b/hwilib/devices/trezorlib/messages/__init__.py @@ -26,7 +26,7 @@ from .Features import Features from .FirmwareErase import FirmwareErase from .FirmwareRequest import FirmwareRequest -from .FirmwareUpload import FirmwareUpload +from .FirmwareUpload import FirmwareUpload, FirmwareUploadKeepkey from .GetAddress import GetAddress from .GetEntropy import GetEntropy from .GetFeatures import GetFeatures diff --git a/hwilib/devices/trezorlib/ui.py b/hwilib/devices/trezorlib/ui.py index dafcae146..182f16640 100644 --- a/hwilib/devices/trezorlib/ui.py +++ b/hwilib/devices/trezorlib/ui.py @@ -67,7 +67,7 @@ def __init__(self, passphrase): def button_request(self, code): if not self.prompt_shown: - echo("Please confirm action on your Trezor device") + echo("Please confirm action on your device") if not self.always_prompt: self.prompt_shown = True diff --git a/hwilib/firmware.py b/hwilib/firmware.py new file mode 100644 index 000000000..c9ecb7998 --- /dev/null +++ b/hwilib/firmware.py @@ -0,0 +1,196 @@ +# Firmware download things + +import datetime +import feedparser +import json +import logging +import os +import re +import requests +import sys + +from urllib.parse import urlparse + +from . import __version__ +from .cli import HWIArgumentParser +from .errors import BadArgumentError, handle_errors, UnknownDeviceError + +def format_success(model, fw_version, filepath): + return {'success': True, 'message': '{} firmware version {} downloaded to {}'.format(model, fw_version, filepath), 'filepath': filepath} + +def _download_file(url): + filename = os.path.basename(urlparse(url).path) + + with requests.get(url, stream=True) as r: + r.raise_for_status() + with open(filename, 'wb') as f: + for chunk in r.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + + return os.path.abspath(filename) + +def _trezor_download(version=None, bitcoinonly=False, device_version=1): + releases = requests.get('https://wallet.trezor.io/data/firmware/{}/releases.json'.format(device_version)).json() + if not releases: + raise IOError('Could not get list of releases') + + if bitcoinonly: + releases = [r for r in releases if "url_bitcoinonly" in r] + releases.sort(key=lambda r: r["version"], reverse=True) + + version_info = {} + if version is None: + version_info = releases[0] + version = '.'.join([str(x) for x in version_info['version']]) + else: + version_list = [int(x) for x in version.split(".")] + for r in releases: + if r['version'] == version_list: + version_info = r + break + else: + raise BadArgumentError('{} is not available'.format(version)) + + url = 'https://wallet.trezor.io/{}'.format(version_info['url_bitcoinonly'] if bitcoinonly else version_info['url']) + downloaded_file = _download_file(url) + + model = 'Trezor ' + if device_version == 1: + model += '1' + elif device_version == 2: + model += 'T' + else: + raise BadArgumentError('Unknown device_version {}'.format(device_version)) + if bitcoinonly: + model += ' Bitcoin only' + + return format_success(model, version, downloaded_file) + +def trezor_1_download(version=None, bitcoinonly=False): + return _trezor_download(version, bitcoinonly, 1) + +def trezor_t_download(version=None, bitcoinonly=False): + return _trezor_download(version, bitcoinonly, 2) + +def coldcard_download(version=None, bitcoinonly=False): + versions = feedparser.parse('https://github.com/Coldcard/firmware/tags.atom') + releases = versions.entries + + def coldcard_version_formatted(ver_str): + try: + return bool(datetime.datetime.strptime(ver_str[:15], '%Y-%m-%dT%H%M')) + except: + return False + + releases = [r for r in releases if coldcard_version_formatted(r['title'])] + releases.sort(key=lambda r: r["updated_parsed"], reverse=True) + + version_info = {} + if version is None: + version_info = releases[0] + version = version_info['title'][17:] + else: + for r in releases: + if r['title'][15:] == '-v{}'.format(version): + version_info = r + break + else: + raise BadArgumentError('{} is not available'.format(version)) + + filename = '{}-coldcard.dfu'.format(version_info['title']) + url = 'https://github.com/Coldcard/firmware/blob/master/releases/{}?raw=true'.format(filename) + downloaded_file = _download_file(url) + + return format_success('Coldcard', version, downloaded_file) + +def keepkey_download(version=None, bitcoinonly=False): + versions = feedparser.parse('https://github.com/keepkey/keepkey-firmware/tags.atom') + releases = versions.entries + + def keepkey_id_formatted(id_str): + tag = id_str.split('/')[-1] + p = re.compile(r'^v\d+.\d+.\d$') + return bool(p.match(tag)) + + releases = [r for r in releases if keepkey_id_formatted(r['id'])] + releases.sort(key=lambda r: r["updated_parsed"], reverse=True) + + version_info = {} + if version is None: + version_info = releases[0] + version = version_info['id'].split('/')[-1][1:] + else: + for r in releases: + if r['id'].split('/')[-1][1:] == version: + version_info = r + break + else: + raise BadArgumentError('{} is not available'.format(version)) + + url = 'https://github.com/keepkey/keepkey-firmware/releases/download/v{}/firmware.keepkey.bin'.format(version) + downloaded_file = _download_file(url) + + return format_success('Keepkey', version, downloaded_file) + +def digitalbitbox_01_download(version=None, bitcoinonly=False): + versions = feedparser.parse('https://github.com/digitalbitbox/mcu/tags.atom') + releases = versions.entries + + def id_formatted(id_str): + tag = id_str.split('/')[-1] + p = re.compile(r'^v\d+.\d+.\d$') + return bool(p.match(tag)) + + releases = [r for r in releases if id_formatted(r['id'])] + releases.sort(key=lambda r: r["updated_parsed"], reverse=True) + + version_info = {} + if version is None: + version_info = releases[0] + version = version_info['id'].split('/')[-1][1:] + else: + for r in releases: + if r['id'].split('/')[-1][1:] == version: + version_info = r + break + else: + raise BadArgumentError('{} is not available'.format(version)) + + url = 'https://github.com/digitalbitbox/mcu/releases/download/v{}/firmware.deterministic.{}.signed.bin'.format(version, version) + downloaded_file = _download_file(url) + + return format_success('Digital Bitbox01', version, downloaded_file) + +def download_firmware(model, version, bitcoinonly=False): + dev_model = model.lower() + func_name = dev_model + '_download' + + try: + dl_func = globals()[func_name] + return dl_func(version, bitcoinonly) + except KeyError: + raise UnknownDeviceError('No Download function for {}'.format(dev_model)) + +def process_commands(cli_args): + parser = HWIArgumentParser(description='Hardware Wallet Interface Firmware Updater and Downloader, version {}.\nDownload and update firmware for harware wallets. Responses are in JSON format.'.format(__version__)) + parser.add_argument('model', help='The name of the device model you want to download firmware for') + parser.add_argument('--firmware-version', '-f', help='The version number to download. If ommitted, download the latest.') + parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__)) + parser.add_argument('--debug', help='Print debug statements', action='store_true') + parser.add_argument('--bitcoinonly', help='Download the Bitcoin only firmware if it is available', action='store_true') + args = parser.parse_args(cli_args) + + # Setup debug logging + logging.basicConfig(level=logging.DEBUG if args.debug else logging.WARNING) + + # Do the commands + result = {} + with handle_errors(result=result, debug=args.debug): + result = download_firmware(args.model, args.firmware_version, args.bitcoinonly) + + return result + +def main(): + result = process_commands(sys.argv[1:]) + print(json.dumps(result)) diff --git a/hwilib/hwwclient.py b/hwilib/hwwclient.py index e27db11f9..73057fad7 100644 --- a/hwilib/hwwclient.py +++ b/hwilib/hwwclient.py @@ -185,3 +185,16 @@ def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]: """ raise NotImplementedError("The HardwareWalletClient base class " "does not implement this method") + + # Verify firmware file then load it onto device + def update_firmware(self, filename: str) -> Dict[str, bool]: + """ + Update firmware + + Must return a dictionary with the "success" key, + possibly including also "error" and "code", e.g.: + {"success": bool, "error": srt, "code": int}. + + Raise UnavailableActionError if appropriate for the device. + """ + raise NotImplementedError('The HardwareWalletClient base class does not implement this method') diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index 81f6e7b9f..000000000 --- a/poetry.lock +++ /dev/null @@ -1,627 +0,0 @@ -[[package]] -name = "altgraph" -version = "0.17" -description = "Python graph (network) package" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "autopep8" -version = "1.5.4" -description = "A tool that automatically formats Python code to conform to the PEP 8 style guide" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -pycodestyle = ">=2.6.0" -toml = "*" - -[[package]] -name = "base58" -version = "2.0.1" -description = "Base58 and Base58Check implementation" -category = "main" -optional = false -python-versions = ">=3.5" - -[[package]] -name = "bitbox02" -version = "4.1.0" -description = "Python library for bitbox02 communication" -category = "main" -optional = false -python-versions = ">=3.6" - -[package.dependencies] -base58 = ">=2.0.0" -ecdsa = ">=0.13" -hidapi = ">=0.7.99.post21" -noiseprotocol = ">=0.3" -protobuf = ">=3.7" -semver = ">=2.8.1" -typing-extensions = ">=3.7.4" - -[[package]] -name = "cffi" -version = "1.14.2" -description = "Foreign Function Interface for Python calling C code." -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cryptography" -version = "3.1" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -category = "main" -optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*" - -[package.dependencies] -cffi = ">=1.8,<1.11.3 || >1.11.3" -six = ">=1.4.1" - -[package.extras] -docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] -pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["pytest (>=3.6.0,!=3.9.0,!=3.9.1,!=3.9.2)", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"] - -[[package]] -name = "ecdsa" -version = "0.16.0" -description = "ECDSA cryptographic signature library (pure python)" -category = "main" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[package.dependencies] -six = ">=1.9.0" - -[package.extras] -gmpy = ["gmpy"] -gmpy2 = ["gmpy2"] - -[[package]] -name = "flake8" -version = "3.8.3" -description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" - -[package.dependencies] -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.6.0a1,<2.7.0" -pyflakes = ">=2.2.0,<2.3.0" - -[[package]] -name = "future" -version = "0.18.2" -description = "Clean single-source support for Python 3 and 2" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "hidapi" -version = "0.9.0.post3" -description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "importlib-metadata" -version = "1.7.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" - -[package.dependencies] -zipp = ">=0.5" - -[package.extras] -docs = ["sphinx", "rst.linker"] -testing = ["packaging", "pep517", "importlib-resources (>=1.3)"] - -[[package]] -name = "libusb1" -version = "1.8" -description = "Pure-python wrapper for libusb-1.0" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "macholib" -version = "1.14" -description = "Mach-O header analysis and editing" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -altgraph = ">=0.15" - -[[package]] -name = "mccabe" -version = "0.6.1" -description = "McCabe checker, plugin for flake8" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "mnemonic" -version = "0.19" -description = "Implementation of Bitcoin BIP-0039" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "mypy" -version = "0.790" -description = "Optional static typing for Python" -category = "dev" -optional = false -python-versions = ">=3.5" - -[package.dependencies] -mypy-extensions = ">=0.4.3,<0.5.0" -typed-ast = ">=1.4.0,<1.5.0" -typing-extensions = ">=3.7.4" - -[package.extras] -dmypy = ["psutil (>=4.0)"] - -[[package]] -name = "mypy-extensions" -version = "0.4.3" -description = "Experimental type system extensions for programs checked with the mypy typechecker." -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "noiseprotocol" -version = "0.3.1" -description = "Implementation of Noise Protocol Framework" -category = "main" -optional = false -python-versions = "~=3.5" - -[package.dependencies] -cryptography = ">=2.8" - -[[package]] -name = "pefile" -version = "2019.4.18" -description = "Python PE parsing module" -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -future = "*" - -[[package]] -name = "protobuf" -version = "3.13.0" -description = "Protocol Buffers" -category = "main" -optional = false -python-versions = "*" - -[package.dependencies] -six = ">=1.9" - -[[package]] -name = "pyaes" -version = "1.6.1" -description = "Pure-Python Implementation of the AES block-cipher and common modes of operation" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "pycodestyle" -version = "2.6.0" -description = "Python style guide checker" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pycparser" -version = "2.20" -description = "C parser in Python" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyflakes" -version = "2.2.0" -description = "passive checker of Python programs" -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "pyinstaller" -version = "4.0" -description = "PyInstaller bundles a Python application and all its dependencies into a single package." -category = "dev" -optional = false -python-versions = "*" - -[package.dependencies] -altgraph = "*" -macholib = {version = ">=1.8", markers = "sys_platform == \"darwin\""} -pefile = {version = ">=2017.8.1", markers = "sys_platform == \"win32\""} -pyinstaller-hooks-contrib = ">=2020.6" -pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} - -[package.extras] -encryption = ["tinyaes (>=1.0.0)"] -hook_testing = ["pytest (>=2.7.3)", "execnet (>=1.5.0)", "psutil"] - -[[package]] -name = "pyinstaller-hooks-contrib" -version = "2020.7" -description = "Community maintained hooks for PyInstaller" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "pyside2" -version = "5.15.0" -description = "Python bindings for the Qt cross-platform application and UI framework" -category = "main" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" - -[package.dependencies] -shiboken2 = "5.15.0" - -[[package]] -name = "pywin32-ctypes" -version = "0.2.0" -description = "" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "semver" -version = "2.10.2" -description = "Python helper for Semantic Versioning (http://semver.org/)" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[[package]] -name = "shiboken2" -version = "5.15.0" -description = "Python / C++ bindings helper module" -category = "main" -optional = true -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <3.9" - -[[package]] -name = "six" -version = "1.15.0" -description = "Python 2 and 3 compatibility utilities" -category = "main" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" - -[[package]] -name = "toml" -version = "0.10.1" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "typed-ast" -version = "1.4.2" -description = "a fork of Python 2 and 3 ast modules with type comment support" -category = "dev" -optional = false -python-versions = "*" - -[[package]] -name = "typing-extensions" -version = "3.7.4.3" -description = "Backported and Experimental Type Hints for Python 3.5+" -category = "main" -optional = false -python-versions = "*" - -[[package]] -name = "zipp" -version = "3.1.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.6" - -[package.extras] -docs = ["sphinx", "jaraco.packaging (>=3.2)", "rst.linker (>=1.9)"] -testing = ["jaraco.itertools", "func-timeout"] - -[extras] -qt = ["pyside2"] - -[metadata] -lock-version = "1.1" -python-versions = "^3.6,<3.9" -content-hash = "2867cd5b6d378c1def9404864aedde43c3a14dcd0a7c9f5d10f5469144c27f91" - -[metadata.files] -altgraph = [ - {file = "altgraph-0.17-py2.py3-none-any.whl", hash = "sha256:c623e5f3408ca61d4016f23a681b9adb100802ca3e3da5e718915a9e4052cebe"}, - {file = "altgraph-0.17.tar.gz", hash = "sha256:1f05a47122542f97028caf78775a095fbe6a2699b5089de8477eb583167d69aa"}, -] -autopep8 = [ - {file = "autopep8-1.5.4.tar.gz", hash = "sha256:d21d3901cb0da6ebd1e83fc9b0dfbde8b46afc2ede4fe32fbda0c7c6118ca094"}, -] -base58 = [ - {file = "base58-2.0.1-py3-none-any.whl", hash = "sha256:447adc750d6b642987ffc6d397ecd15a799852d5f6a1d308d384500243825058"}, - {file = "base58-2.0.1.tar.gz", hash = "sha256:365c9561d9babac1b5f18ee797508cd54937a724b6e419a130abad69cec5ca79"}, -] -bitbox02 = [ - {file = "bitbox02-4.1.0-py3-none-any.whl", hash = "sha256:1af95952d67b74c80ccc0588e0aee983c764960da637bd24bc41a1cb89d5e127"}, - {file = "bitbox02-4.1.0.tar.gz", hash = "sha256:73a35594162f32897dd2b1880f0cfaa42922acd1c2d7f4cf3d94b8333329c931"}, -] -cffi = [ - {file = "cffi-1.14.2-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:da9d3c506f43e220336433dffe643fbfa40096d408cb9b7f2477892f369d5f82"}, - {file = "cffi-1.14.2-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23e44937d7695c27c66a54d793dd4b45889a81b35c0751ba91040fe825ec59c4"}, - {file = "cffi-1.14.2-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:0da50dcbccd7cb7e6c741ab7912b2eff48e85af217d72b57f80ebc616257125e"}, - {file = "cffi-1.14.2-cp27-cp27m-win32.whl", hash = "sha256:76ada88d62eb24de7051c5157a1a78fd853cca9b91c0713c2e973e4196271d0c"}, - {file = "cffi-1.14.2-cp27-cp27m-win_amd64.whl", hash = "sha256:15a5f59a4808f82d8ec7364cbace851df591c2d43bc76bcbe5c4543a7ddd1bf1"}, - {file = "cffi-1.14.2-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:e4082d832e36e7f9b2278bc774886ca8207346b99f278e54c9de4834f17232f7"}, - {file = "cffi-1.14.2-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:57214fa5430399dffd54f4be37b56fe22cedb2b98862550d43cc085fb698dc2c"}, - {file = "cffi-1.14.2-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:6843db0343e12e3f52cc58430ad559d850a53684f5b352540ca3f1bc56df0731"}, - {file = "cffi-1.14.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:577791f948d34d569acb2d1add5831731c59d5a0c50a6d9f629ae1cefd9ca4a0"}, - {file = "cffi-1.14.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8662aabfeab00cea149a3d1c2999b0731e70c6b5bac596d95d13f643e76d3d4e"}, - {file = "cffi-1.14.2-cp35-cp35m-win32.whl", hash = "sha256:837398c2ec00228679513802e3744d1e8e3cb1204aa6ad408b6aff081e99a487"}, - {file = "cffi-1.14.2-cp35-cp35m-win_amd64.whl", hash = "sha256:bf44a9a0141a082e89c90e8d785b212a872db793a0080c20f6ae6e2a0ebf82ad"}, - {file = "cffi-1.14.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:29c4688ace466a365b85a51dcc5e3c853c1d283f293dfcc12f7a77e498f160d2"}, - {file = "cffi-1.14.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:99cc66b33c418cd579c0f03b77b94263c305c389cb0c6972dac420f24b3bf123"}, - {file = "cffi-1.14.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:65867d63f0fd1b500fa343d7798fa64e9e681b594e0a07dc934c13e76ee28fb1"}, - {file = "cffi-1.14.2-cp36-cp36m-win32.whl", hash = "sha256:f5033952def24172e60493b68717792e3aebb387a8d186c43c020d9363ee7281"}, - {file = "cffi-1.14.2-cp36-cp36m-win_amd64.whl", hash = "sha256:7057613efefd36cacabbdbcef010e0a9c20a88fc07eb3e616019ea1692fa5df4"}, - {file = "cffi-1.14.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:6539314d84c4d36f28d73adc1b45e9f4ee2a89cdc7e5d2b0a6dbacba31906798"}, - {file = "cffi-1.14.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:672b539db20fef6b03d6f7a14b5825d57c98e4026401fce838849f8de73fe4d4"}, - {file = "cffi-1.14.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:95e9094162fa712f18b4f60896e34b621df99147c2cee216cfa8f022294e8e9f"}, - {file = "cffi-1.14.2-cp37-cp37m-win32.whl", hash = "sha256:b9aa9d8818c2e917fa2c105ad538e222a5bce59777133840b93134022a7ce650"}, - {file = "cffi-1.14.2-cp37-cp37m-win_amd64.whl", hash = "sha256:e4b9b7af398c32e408c00eb4e0d33ced2f9121fd9fb978e6c1b57edd014a7d15"}, - {file = "cffi-1.14.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e613514a82539fc48291d01933951a13ae93b6b444a88782480be32245ed4afa"}, - {file = "cffi-1.14.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:9b219511d8b64d3fa14261963933be34028ea0e57455baf6781fe399c2c3206c"}, - {file = "cffi-1.14.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:c0b48b98d79cf795b0916c57bebbc6d16bb43b9fc9b8c9f57f4cf05881904c75"}, - {file = "cffi-1.14.2-cp38-cp38-win32.whl", hash = "sha256:15419020b0e812b40d96ec9d369b2bc8109cc3295eac6e013d3261343580cc7e"}, - {file = "cffi-1.14.2-cp38-cp38-win_amd64.whl", hash = "sha256:12a453e03124069b6896107ee133ae3ab04c624bb10683e1ed1c1663df17c13c"}, - {file = "cffi-1.14.2.tar.gz", hash = "sha256:ae8f34d50af2c2154035984b8b5fc5d9ed63f32fe615646ab435b05b132ca91b"}, -] -cryptography = [ - {file = "cryptography-3.1-cp27-cp27m-macosx_10_10_x86_64.whl", hash = "sha256:969ae512a250f869c1738ca63be843488ff5cc031987d302c1f59c7dbe1b225f"}, - {file = "cryptography-3.1-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:b45ab1c6ece7c471f01c56f5d19818ca797c34541f0b2351635a5c9fe09ac2e0"}, - {file = "cryptography-3.1-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:247df238bc05c7d2e934a761243bfdc67db03f339948b1e2e80c75d41fc7cc36"}, - {file = "cryptography-3.1-cp27-cp27m-win32.whl", hash = "sha256:10c9775a3f31610cf6b694d1fe598f2183441de81cedcf1814451ae53d71b13a"}, - {file = "cryptography-3.1-cp27-cp27m-win_amd64.whl", hash = "sha256:9f734423eb9c2ea85000aa2476e0d7a58e021bc34f0a373ac52a5454cd52f791"}, - {file = "cryptography-3.1-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:e7563eb7bc5c7e75a213281715155248cceba88b11cb4b22957ad45b85903761"}, - {file = "cryptography-3.1-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:94191501e4b4009642be21dde2a78bd3c2701a81ee57d3d3d02f1d99f8b64a9e"}, - {file = "cryptography-3.1-cp35-abi3-macosx_10_10_x86_64.whl", hash = "sha256:dc3f437ca6353979aace181f1b790f0fc79e446235b14306241633ab7d61b8f8"}, - {file = "cryptography-3.1-cp35-abi3-manylinux1_x86_64.whl", hash = "sha256:725875681afe50b41aee7fdd629cedbc4720bab350142b12c55c0a4d17c7416c"}, - {file = "cryptography-3.1-cp35-abi3-manylinux2010_x86_64.whl", hash = "sha256:321761d55fb7cb256b771ee4ed78e69486a7336be9143b90c52be59d7657f50f"}, - {file = "cryptography-3.1-cp35-abi3-manylinux2014_aarch64.whl", hash = "sha256:2a27615c965173c4c88f2961cf18115c08fedfb8bdc121347f26e8458dc6d237"}, - {file = "cryptography-3.1-cp35-cp35m-win32.whl", hash = "sha256:e7dad66a9e5684a40f270bd4aee1906878193ae50a4831922e454a2a457f1716"}, - {file = "cryptography-3.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4005b38cd86fc51c955db40b0f0e52ff65340874495af72efabb1bb8ca881695"}, - {file = "cryptography-3.1-cp36-abi3-win32.whl", hash = "sha256:cc6096c86ec0de26e2263c228fb25ee01c3ff1346d3cfc219d67d49f303585af"}, - {file = "cryptography-3.1-cp36-abi3-win_amd64.whl", hash = "sha256:2e26223ac636ca216e855748e7d435a1bf846809ed12ed898179587d0cf74618"}, - {file = "cryptography-3.1-cp36-cp36m-win32.whl", hash = "sha256:7a63e97355f3cd77c94bd98c59cb85fe0efd76ea7ef904c9b0316b5bbfde6ed1"}, - {file = "cryptography-3.1-cp36-cp36m-win_amd64.whl", hash = "sha256:4b9e96543d0784acebb70991ebc2dbd99aa287f6217546bb993df22dd361d41c"}, - {file = "cryptography-3.1-cp37-cp37m-win32.whl", hash = "sha256:eb80a288e3cfc08f679f95da72d2ef90cb74f6d8a8ba69d2f215c5e110b2ca32"}, - {file = "cryptography-3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:180c9f855a8ea280e72a5d61cf05681b230c2dce804c48e9b2983f491ecc44ed"}, - {file = "cryptography-3.1-cp38-cp38-win32.whl", hash = "sha256:fa7fbcc40e2210aca26c7ac8a39467eae444d90a2c346cbcffd9133a166bcc67"}, - {file = "cryptography-3.1-cp38-cp38-win_amd64.whl", hash = "sha256:548b0818e88792318dc137d8b1ec82a0ab0af96c7f0603a00bb94f896fbf5e10"}, - {file = "cryptography-3.1.tar.gz", hash = "sha256:26409a473cc6278e4c90f782cd5968ebad04d3911ed1c402fc86908c17633e08"}, -] -ecdsa = [ - {file = "ecdsa-0.16.0-py2.py3-none-any.whl", hash = "sha256:ca359c971594dceebf334f3d623dae43163ab161c7d09f28cae70a86df26eb7a"}, - {file = "ecdsa-0.16.0.tar.gz", hash = "sha256:494c6a853e9ed2e9be33d160b41d47afc50a6629b993d2b9c5ad7bb226add892"}, -] -flake8 = [ - {file = "flake8-3.8.3-py2.py3-none-any.whl", hash = "sha256:15e351d19611c887e482fb960eae4d44845013cc142d42896e9862f775d8cf5c"}, - {file = "flake8-3.8.3.tar.gz", hash = "sha256:f04b9fcbac03b0a3e58c0ab3a0ecc462e023a9faf046d57794184028123aa208"}, -] -future = [ - {file = "future-0.18.2.tar.gz", hash = "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d"}, -] -hidapi = [ - {file = "hidapi-0.9.0.post3-cp35-cp35m-win32.whl", hash = "sha256:98bada9a2625a90a452b17b237a342c29142677c77dd0ba96072f45b0e55d5ec"}, - {file = "hidapi-0.9.0.post3-cp35-cp35m-win_amd64.whl", hash = "sha256:82d6276337d7cc25acda8b5fa99e0db497090c369611eefa18ea69c9afe55ed7"}, - {file = "hidapi-0.9.0.post3-cp36-cp36m-win32.whl", hash = "sha256:92995887078d8e7b768a60b597d1117b1aba0a5184538b633be7192daeba34cc"}, - {file = "hidapi-0.9.0.post3-cp36-cp36m-win_amd64.whl", hash = "sha256:4ee5bf9f2ece8ac73ef01f0a56ea6f62dcf024ba3beba6b29d3d52d96112931e"}, - {file = "hidapi-0.9.0.post3-cp37-cp37m-win32.whl", hash = "sha256:12288a950d7c7c3756f25405b74eb17ad84032c06a65bbbe78adea8dd247f4c0"}, - {file = "hidapi-0.9.0.post3-cp37-cp37m-win_amd64.whl", hash = "sha256:3910117ee13f3730f6810cf4b591f84dc4b55258163cbbdf9135b55deced1775"}, - {file = "hidapi-0.9.0.post3-cp38-cp38-win32.whl", hash = "sha256:a1bf3893353f654613fecc10259097d417e76ff8799f3be459aed7d1e9cee7fd"}, - {file = "hidapi-0.9.0.post3-cp38-cp38-win_amd64.whl", hash = "sha256:f70e0609c36605d3c06a91fbccc058e255918af2c59872648fe551360ad68df5"}, - {file = "hidapi-0.9.0.post3.tar.gz", hash = "sha256:5a2442928f17ba742d9c53073f48b152051c5747d758d2fefd937543da5ab2e5"}, -] -importlib-metadata = [ - {file = "importlib_metadata-1.7.0-py2.py3-none-any.whl", hash = "sha256:dc15b2969b4ce36305c51eebe62d418ac7791e9a157911d58bfb1f9ccd8e2070"}, - {file = "importlib_metadata-1.7.0.tar.gz", hash = "sha256:90bb658cdbbf6d1735b6341ce708fc7024a3e14e99ffdc5783edea9f9b077f83"}, -] -libusb1 = [ - {file = "libusb1-1.8.tar.gz", hash = "sha256:240f65ac70ba3fab77749ec84a412e4e89624804cb80d6c9d394eef5af8878d6"}, -] -macholib = [ - {file = "macholib-1.14-py2.py3-none-any.whl", hash = "sha256:c500f02867515e6c60a27875b408920d18332ddf96b4035ef03beddd782d4281"}, - {file = "macholib-1.14.tar.gz", hash = "sha256:0c436bc847e7b1d9bda0560351bf76d7caf930fb585a828d13608839ef42c432"}, -] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] -mnemonic = [ - {file = "mnemonic-0.19-py2.py3-none-any.whl", hash = "sha256:a8d78c5100acfa7df9bab6b9db7390831b0e54490934b718ff9efd68f0d731a6"}, - {file = "mnemonic-0.19.tar.gz", hash = "sha256:4e37eb02b2cbd56a0079cabe58a6da93e60e3e4d6e757a586d9f23d96abea931"}, -] -mypy = [ - {file = "mypy-0.790-cp35-cp35m-macosx_10_6_x86_64.whl", hash = "sha256:bd03b3cf666bff8d710d633d1c56ab7facbdc204d567715cb3b9f85c6e94f669"}, - {file = "mypy-0.790-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:2170492030f6faa537647d29945786d297e4862765f0b4ac5930ff62e300d802"}, - {file = "mypy-0.790-cp35-cp35m-win_amd64.whl", hash = "sha256:e86bdace26c5fe9cf8cb735e7cedfe7850ad92b327ac5d797c656717d2ca66de"}, - {file = "mypy-0.790-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:e97e9c13d67fbe524be17e4d8025d51a7dca38f90de2e462243ab8ed8a9178d1"}, - {file = "mypy-0.790-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:0d34d6b122597d48a36d6c59e35341f410d4abfa771d96d04ae2c468dd201abc"}, - {file = "mypy-0.790-cp36-cp36m-win_amd64.whl", hash = "sha256:72060bf64f290fb629bd4a67c707a66fd88ca26e413a91384b18db3876e57ed7"}, - {file = "mypy-0.790-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:eea260feb1830a627fb526d22fbb426b750d9f5a47b624e8d5e7e004359b219c"}, - {file = "mypy-0.790-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c614194e01c85bb2e551c421397e49afb2872c88b5830e3554f0519f9fb1c178"}, - {file = "mypy-0.790-cp37-cp37m-win_amd64.whl", hash = "sha256:0a0d102247c16ce93c97066443d11e2d36e6cc2a32d8ccc1f705268970479324"}, - {file = "mypy-0.790-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cf4e7bf7f1214826cf7333627cb2547c0db7e3078723227820d0a2490f117a01"}, - {file = "mypy-0.790-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:af4e9ff1834e565f1baa74ccf7ae2564ae38c8df2a85b057af1dbbc958eb6666"}, - {file = "mypy-0.790-cp38-cp38-win_amd64.whl", hash = "sha256:da56dedcd7cd502ccd3c5dddc656cb36113dd793ad466e894574125945653cea"}, - {file = "mypy-0.790-py3-none-any.whl", hash = "sha256:2842d4fbd1b12ab422346376aad03ff5d0805b706102e475e962370f874a5122"}, - {file = "mypy-0.790.tar.gz", hash = "sha256:2b21ba45ad9ef2e2eb88ce4aeadd0112d0f5026418324176fd494a6824b74975"}, -] -mypy-extensions = [ - {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, - {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, -] -noiseprotocol = [ - {file = "noiseprotocol-0.3.1-py3-none-any.whl", hash = "sha256:2e1a603a38439636cf0ffd8b3e8b12cee27d368a28b41be7dbe568b2abb23111"}, -] -pefile = [ - {file = "pefile-2019.4.18.tar.gz", hash = "sha256:a5d6e8305c6b210849b47a6174ddf9c452b2888340b8177874b862ba6c207645"}, -] -protobuf = [ - {file = "protobuf-3.13.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9c2e63c1743cba12737169c447374fab3dfeb18111a460a8c1a000e35836b18c"}, - {file = "protobuf-3.13.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:1e834076dfef9e585815757a2c7e4560c7ccc5962b9d09f831214c693a91b463"}, - {file = "protobuf-3.13.0-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:df3932e1834a64b46ebc262e951cd82c3cf0fa936a154f0a42231140d8237060"}, - {file = "protobuf-3.13.0-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:8c35bcbed1c0d29b127c886790e9d37e845ffc2725cc1db4bd06d70f4e8359f4"}, - {file = "protobuf-3.13.0-cp35-cp35m-win32.whl", hash = "sha256:339c3a003e3c797bc84499fa32e0aac83c768e67b3de4a5d7a5a9aa3b0da634c"}, - {file = "protobuf-3.13.0-cp35-cp35m-win_amd64.whl", hash = "sha256:361acd76f0ad38c6e38f14d08775514fbd241316cce08deb2ce914c7dfa1184a"}, - {file = "protobuf-3.13.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9edfdc679a3669988ec55a989ff62449f670dfa7018df6ad7f04e8dbacb10630"}, - {file = "protobuf-3.13.0-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:5db9d3e12b6ede5e601b8d8684a7f9d90581882925c96acf8495957b4f1b204b"}, - {file = "protobuf-3.13.0-cp36-cp36m-win32.whl", hash = "sha256:c8abd7605185836f6f11f97b21200f8a864f9cb078a193fe3c9e235711d3ff1e"}, - {file = "protobuf-3.13.0-cp36-cp36m-win_amd64.whl", hash = "sha256:4d1174c9ed303070ad59553f435846a2f877598f59f9afc1b89757bdf846f2a7"}, - {file = "protobuf-3.13.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bba42f439bf45c0f600c3c5993666fcb88e8441d011fad80a11df6f324eef33"}, - {file = "protobuf-3.13.0-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:c0c5ab9c4b1eac0a9b838f1e46038c3175a95b0f2d944385884af72876bd6bc7"}, - {file = "protobuf-3.13.0-cp37-cp37m-win32.whl", hash = "sha256:f68eb9d03c7d84bd01c790948320b768de8559761897763731294e3bc316decb"}, - {file = "protobuf-3.13.0-cp37-cp37m-win_amd64.whl", hash = "sha256:91c2d897da84c62816e2f473ece60ebfeab024a16c1751aaf31100127ccd93ec"}, - {file = "protobuf-3.13.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3dee442884a18c16d023e52e32dd34a8930a889e511af493f6dc7d4d9bf12e4f"}, - {file = "protobuf-3.13.0-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:e7662437ca1e0c51b93cadb988f9b353fa6b8013c0385d63a70c8a77d84da5f9"}, - {file = "protobuf-3.13.0-py2.py3-none-any.whl", hash = "sha256:d69697acac76d9f250ab745b46c725edf3e98ac24763990b24d58c16c642947a"}, - {file = "protobuf-3.13.0.tar.gz", hash = "sha256:6a82e0c8bb2bf58f606040cc5814e07715b2094caeba281e2e7d0b0e2e397db5"}, -] -pyaes = [ - {file = "pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f"}, -] -pycodestyle = [ - {file = "pycodestyle-2.6.0-py2.py3-none-any.whl", hash = "sha256:2295e7b2f6b5bd100585ebcb1f616591b652db8a741695b3d8f5d28bdc934367"}, - {file = "pycodestyle-2.6.0.tar.gz", hash = "sha256:c58a7d2815e0e8d7972bf1803331fb0152f867bd89adf8a01dfd55085434192e"}, -] -pycparser = [ - {file = "pycparser-2.20-py2.py3-none-any.whl", hash = "sha256:7582ad22678f0fcd81102833f60ef8d0e57288b6b5fb00323d101be910e35705"}, - {file = "pycparser-2.20.tar.gz", hash = "sha256:2d475327684562c3a96cc71adf7dc8c4f0565175cf86b6d7a404ff4c771f15f0"}, -] -pyflakes = [ - {file = "pyflakes-2.2.0-py2.py3-none-any.whl", hash = "sha256:0d94e0e05a19e57a99444b6ddcf9a6eb2e5c68d3ca1e98e90707af8152c90a92"}, - {file = "pyflakes-2.2.0.tar.gz", hash = "sha256:35b2d75ee967ea93b55750aa9edbbf72813e06a66ba54438df2cfac9e3c27fc8"}, -] -pyinstaller = [ - {file = "pyinstaller-4.0.tar.gz", hash = "sha256:970beb07115761d5e4ec317c1351b712fd90ae7f23994db914c633281f99bab0"}, -] -pyinstaller-hooks-contrib = [ - {file = "pyinstaller-hooks-contrib-2020.7.tar.gz", hash = "sha256:74936d044f319cd7a9dca322b46a818fcb6e2af1c67af62e8a6a3121eb2863d2"}, - {file = "pyinstaller_hooks_contrib-2020.7-py2.py3-none-any.whl", hash = "sha256:5b6e06ba6072499189f5b8e1623d5f0414962941aac370ee4f842de25455be5b"}, -] -pyside2 = [ - {file = "PySide2-5.15.0-5.15.0-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:ae8158d611a410c58091aa8baf24005894b4e3f40c63ff2482149481ad5395b4"}, - {file = "PySide2-5.15.0-5.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:de0220cc01a8bfdaa8ccd0fc934a1ead2aedca62b49b5fd4bdcdaba6f4585a03"}, - {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:1478ea8a1ab5d8bc021ce41211933fbc238338fe70c02f7bcc2e80ea900dbf9e"}, - {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:f9099e49fb2d3571f5a81eb9ff281ce832ce8c333052e8175e2356b9c3e4a882"}, - {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:7c91a5074f3c60bac7e9336943a1dc9d5c8be8ab88a232dc55018e555dae81b2"}, - {file = "PySide2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:2d72150f63025b9b55097c1a64d09da37ff9191f73f69237500dec7a4a130541"}, -] -pywin32-ctypes = [ - {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"}, - {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"}, -] -semver = [ - {file = "semver-2.10.2-py2.py3-none-any.whl", hash = "sha256:21e80ca738975ed513cba859db0a0d2faca2380aef1962f48272ebf9a8a44bd4"}, - {file = "semver-2.10.2.tar.gz", hash = "sha256:c0a4a9d1e45557297a722ee9bac3de2ec2ea79016b6ffcaca609b0bc62cf4276"}, -] -shiboken2 = [ - {file = "shiboken2-5.15.0-5.15.0-cp27-cp27m-macosx_10_13_intel.whl", hash = "sha256:0826ce788fe55bce19a8f8a2c33d720a6ba8f59e1aab1fa9d7a53eceed3f3af5"}, - {file = "shiboken2-5.15.0-5.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:a92c55363d5cd3cfdd6cd28dcf91e81a00a3aa5bb177d712817c09d26bd760db"}, - {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-macosx_10_13_intel.whl", hash = "sha256:41a9157fb9cc7e0c0747926b25c23c3f94d59d61736a6ff763ebc7acf6afc5cf"}, - {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-abi3-manylinux1_x86_64.whl", hash = "sha256:5702e77ad5999ac45498c3cd47f5d078ce7406cf8dc8df74337b0cdc084bf762"}, - {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win32.whl", hash = "sha256:4b0904e0967356a36e80cde05981faa14c120141856d973ee983eac0b83633c0"}, - {file = "shiboken2-5.15.0-5.15.0-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:19d5f715e5ae8a815a7f148a8614a3225dceee6fd9d5decaa7749657f0f7ccbe"}, - {file = "shiboken2-5.15.0-5.15.0_1-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:94991848e9ff4d03c2d7feab484113b5b5ad7f9fdfa0b0ff46ce18da47b36b58"}, - {file = "shiboken2-5.15.0-5.15.0_2-cp35.cp36.cp37.cp38-none-win_amd64.whl", hash = "sha256:e753324a78cbdab1c5917b5600c708a8db7e1336579e7afa20ed90edda15eefa"}, -] -six = [ - {file = "six-1.15.0-py2.py3-none-any.whl", hash = "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"}, - {file = "six-1.15.0.tar.gz", hash = "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259"}, -] -toml = [ - {file = "toml-0.10.1-py2.py3-none-any.whl", hash = "sha256:bda89d5935c2eac546d648028b9901107a595863cb36bae0c73ac804a9b4ce88"}, - {file = "toml-0.10.1.tar.gz", hash = "sha256:926b612be1e5ce0634a2ca03470f95169cf16f939018233a670519cb4ac58b0f"}, -] -typed-ast = [ - {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:7703620125e4fb79b64aa52427ec192822e9f45d37d4b6625ab37ef403e1df70"}, - {file = "typed_ast-1.4.2-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:c9aadc4924d4b5799112837b226160428524a9a45f830e0d0f184b19e4090487"}, - {file = "typed_ast-1.4.2-cp35-cp35m-manylinux2014_aarch64.whl", hash = "sha256:9ec45db0c766f196ae629e509f059ff05fc3148f9ffd28f3cfe75d4afb485412"}, - {file = "typed_ast-1.4.2-cp35-cp35m-win32.whl", hash = "sha256:85f95aa97a35bdb2f2f7d10ec5bbdac0aeb9dafdaf88e17492da0504de2e6400"}, - {file = "typed_ast-1.4.2-cp35-cp35m-win_amd64.whl", hash = "sha256:9044ef2df88d7f33692ae3f18d3be63dec69c4fb1b5a4a9ac950f9b4ba571606"}, - {file = "typed_ast-1.4.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c1c876fd795b36126f773db9cbb393f19808edd2637e00fd6caba0e25f2c7b64"}, - {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:5dcfc2e264bd8a1db8b11a892bd1647154ce03eeba94b461effe68790d8b8e07"}, - {file = "typed_ast-1.4.2-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:8db0e856712f79c45956da0c9a40ca4246abc3485ae0d7ecc86a20f5e4c09abc"}, - {file = "typed_ast-1.4.2-cp36-cp36m-manylinux2014_aarch64.whl", hash = "sha256:d003156bb6a59cda9050e983441b7fa2487f7800d76bdc065566b7d728b4581a"}, - {file = "typed_ast-1.4.2-cp36-cp36m-win32.whl", hash = "sha256:4c790331247081ea7c632a76d5b2a265e6d325ecd3179d06e9cf8d46d90dd151"}, - {file = "typed_ast-1.4.2-cp36-cp36m-win_amd64.whl", hash = "sha256:d175297e9533d8d37437abc14e8a83cbc68af93cc9c1c59c2c292ec59a0697a3"}, - {file = "typed_ast-1.4.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cf54cfa843f297991b7388c281cb3855d911137223c6b6d2dd82a47ae5125a41"}, - {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:b4fcdcfa302538f70929eb7b392f536a237cbe2ed9cba88e3bf5027b39f5f77f"}, - {file = "typed_ast-1.4.2-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:987f15737aba2ab5f3928c617ccf1ce412e2e321c77ab16ca5a293e7bbffd581"}, - {file = "typed_ast-1.4.2-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:37f48d46d733d57cc70fd5f30572d11ab8ed92da6e6b28e024e4a3edfb456e37"}, - {file = "typed_ast-1.4.2-cp37-cp37m-win32.whl", hash = "sha256:36d829b31ab67d6fcb30e185ec996e1f72b892255a745d3a82138c97d21ed1cd"}, - {file = "typed_ast-1.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8368f83e93c7156ccd40e49a783a6a6850ca25b556c0fa0240ed0f659d2fe496"}, - {file = "typed_ast-1.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:963c80b583b0661918718b095e02303d8078950b26cc00b5e5ea9ababe0de1fc"}, - {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_i686.whl", hash = "sha256:e683e409e5c45d5c9082dc1daf13f6374300806240719f95dc783d1fc942af10"}, - {file = "typed_ast-1.4.2-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:84aa6223d71012c68d577c83f4e7db50d11d6b1399a9c779046d75e24bed74ea"}, - {file = "typed_ast-1.4.2-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:a38878a223bdd37c9709d07cd357bb79f4c760b29210e14ad0fb395294583787"}, - {file = "typed_ast-1.4.2-cp38-cp38-win32.whl", hash = "sha256:a2c927c49f2029291fbabd673d51a2180038f8cd5a5b2f290f78c4516be48be2"}, - {file = "typed_ast-1.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:c0c74e5579af4b977c8b932f40a5464764b2f86681327410aa028a22d2f54937"}, - {file = "typed_ast-1.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:07d49388d5bf7e863f7fa2f124b1b1d89d8aa0e2f7812faff0a5658c01c59aa1"}, - {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_i686.whl", hash = "sha256:240296b27397e4e37874abb1df2a608a92df85cf3e2a04d0d4d61055c8305ba6"}, - {file = "typed_ast-1.4.2-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:d746a437cdbca200622385305aedd9aef68e8a645e385cc483bdc5e488f07166"}, - {file = "typed_ast-1.4.2-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:14bf1522cdee369e8f5581238edac09150c765ec1cb33615855889cf33dcb92d"}, - {file = "typed_ast-1.4.2-cp39-cp39-win32.whl", hash = "sha256:cc7b98bf58167b7f2db91a4327da24fb93368838eb84a44c472283778fc2446b"}, - {file = "typed_ast-1.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:7147e2a76c75f0f64c4319886e7639e490fee87c9d25cb1d4faef1d8cf83a440"}, - {file = "typed_ast-1.4.2.tar.gz", hash = "sha256:9fc0b3cb5d1720e7141d103cf4819aea239f7d136acf9ee4a69b047b7986175a"}, -] -typing-extensions = [ - {file = "typing_extensions-3.7.4.3-py2-none-any.whl", hash = "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"}, - {file = "typing_extensions-3.7.4.3-py3-none-any.whl", hash = "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918"}, - {file = "typing_extensions-3.7.4.3.tar.gz", hash = "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c"}, -] -zipp = [ - {file = "zipp-3.1.0-py3-none-any.whl", hash = "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b"}, - {file = "zipp-3.1.0.tar.gz", hash = "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"}, -] diff --git a/pyproject.toml b/pyproject.toml index 09009c53c..9a64878c8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,9 @@ typing-extensions = "^3.7" libusb1 = "^1.7" pyside2 = { version = "^5.14.0", optional = true } bitbox02 = ">=4.1.0" +construct = "^2.9.47" +feedparser = {version = "^5.2.1", extras = ["firmwaredl"]} +requests = {version = "^2.22.0", extras = ["firmwaredl"]} [tool.poetry.extras] qt = ["pyside2"] @@ -41,6 +44,7 @@ mypy = "^0.790" [tool.poetry.scripts] hwi = 'hwilib.cli:main' hwi-qt = 'hwilib.gui:main' +firmwaredl = 'hwilib.firmware:main' [build-system] requires = ["poetry>=0.12"]