Repository: bitcoin-core/HWI (language: python)

Issue:
bitbox02 should not be a hard dependency
There is a hard install-time dependency on bitbox02 currently,

https://github.com/bitcoin-core/HWI/blob/master/pyproject.toml#L27

And, if it's not installed, `hwi-qt` will fail to run, with an error like,

```
Traceback (most recent call last):
  File "/usr/bin/hwi-qt", line 33, in <module>
    sys.exit(load_entry_point('hwi==2.2.1', 'console_scripts', 'hwi-qt')())
  File "/usr/bin/hwi-qt", line 25, in importlib_load_entry_point
    return next(matches).load()
  File "/usr/lib/python3.10/importlib/metadata/__init__.py", line 171, in load
    module = import_module(match.group('module'))
  File "/usr/lib/python3.10/importlib/__init__.py", line 126, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1050, in _gcd_import
  File "<frozen importlib._bootstrap>", line 1027, in _find_and_load
  File "<frozen importlib._bootstrap>", line 1006, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 688, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 883, in exec_module
  File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
  File "/usr/lib/python3.10/site-packages/hwilib/_gui.py", line 32, in <module>
    import bitbox02.util
  File "/usr/lib/python3.10/site-packages/shiboken2/files.dir/shibokensupport/feature.py", line 139, in _import
    return original_import(name, *args, **kwargs)
ModuleNotFoundError: No module named 'bitbox02'
```

But, you actually can use `hwi` without bitbox02, you just need to ignore a warning

```
% hwi enumerate
WARNING:root:No module named 'bitbox02', required for bitbox02. Ignore if you do not want this device.
WARNING:root:No module named 'cbor', required for jade. Ignore if you do not want this device.
   .. proceeds to work ...
```

For a user that does not have a bitbox device, this is an extra dependency and extra code that is not needed.

I don't believe this package has any other direct dependencies which exist solely to support one specific vendor.

For these reasons, I would like to see the `bitbox02` dependency removed, and changed to runtime detection. That is, if you want to use HWI with bitbox, you install `bitbox02` separately, and HWI notices that at runtime and uses it. Otherwise, you can use HWI with other devices without installing `bitbox02`.

Any support for this idea?



Discussion / hints:
See also - https://aur.archlinux.org/packages/python-hwilib#comment-908596
This could be done with Poetry [extras](https://python-poetry.org/docs/pyproject/#extras), which are [already used](https://github.com/bitcoin-core/HWI/blob/43cc81d3af7e1de20fa8c92570d305440bb0a33b/pyproject.toml#L32-L33) in this project.

Fix patch:
diff --git a/hwilib/_base58.py b/hwilib/_base58.py
index 829e30203..dcfd51181 100644
--- a/hwilib/_base58.py
+++ b/hwilib/_base58.py
@@ -84,6 +84,21 @@ def decode(s: str) -> bytes:
             break
     return b'\x00' * pad + res
 
+def decode_check(s: str) -> bytes:
+    """
+    Decode a Base58Check encoded string, returning bytes
+
+    :param s: Base58 string to decode
+    :return: Bytes encoded by ``s``
+    """
+    data = decode(s)
+    payload = data[:-4]
+    checksum = data[-4:]
+    calc_checksum = hash256(payload)
+    if checksum != calc_checksum:
+        raise ValueError("Invalid checksum")
+    return payload
+
 def get_xpub_fingerprint(s: str) -> bytes:
     """
     Get the parent fingerprint from an extended public key
diff --git a/hwilib/_gui.py b/hwilib/_gui.py
index cb37b4422..5591a8c2b 100644
--- a/hwilib/_gui.py
+++ b/hwilib/_gui.py
@@ -29,8 +29,6 @@
 from PySide2.QtWidgets import QApplication, QDialog, QDialogButtonBox, QLineEdit, QMessageBox, QMainWindow
 from PySide2.QtCore import QCoreApplication, QRegExp, Signal, Slot
 
-import bitbox02.util
-
 def do_command(f, *args, **kwargs):
     result = {}
     with handle_errors(result=result):
@@ -222,52 +220,61 @@ def toggle_account(self, checked):
             self.ui.path_lineedit.setEnabled(True)
             self.ui.account_spinbox.setEnabled(False)
 
-class BitBox02PairingDialog(QDialog):
-    def __init__(self, pairing_code: str, device_response: Callable[[], bool]):
-        super(BitBox02PairingDialog, self).__init__()
-        self.ui = Ui_BitBox02PairingDialog()
-        self.ui.setupUi(self)
-        self.setWindowTitle('Verify BitBox02 pairing code')
-        self.ui.pairingCode.setText(pairing_code.replace("\n", "<br>"))
-        self.ui.buttonBox.setEnabled(False)
-        self.device_response = device_response
-        self.painted = False
-
-    def paintEvent(self, ev):
-        super().paintEvent(ev)
-        self.painted = True
-
-    def enable_buttons(self):
-        self.ui.buttonBox.setEnabled(True)
-
-class BitBox02NoiseConfig(bitbox02.util.BitBoxAppNoiseConfig):
-    """ GUI elements to perform the BitBox02 pairing and attestatoin check """
-
-    def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:
-        dialog = BitBox02PairingDialog(code, device_response)
-        dialog.show()
-        # render the window since the next operation is blocking
-        while True:
-            QCoreApplication.processEvents()
-            if dialog.painted:
-                break
-            time.sleep(0.1)
-        if not device_response():
-            return False
-        dialog.enable_buttons()
-        dialog.exec_()
-        return dialog.result() == QDialog.Accepted
-
-    def attestation_check(self, result: bool) -> None:
-        if not result:
-            QMessageBox.warning(
-                None,
-                "BitBox02 attestation check",
-                "BitBox02 attestation check failed. Your BitBox02 might not be genuine. Please contact support@shiftcrypto.ch if the problem persists.",
-            )
+try:
+    # Try to import bitbox02 things
+    # Not all dependencies may be available, in which case just ignore these two classes
+    # The code that needs this should already be (implicitly) guarded by bitbox02_lib imports working
+    # so these classes will not be referenced in that case.
+    from .devices.bitbox02_lib.util import BitBoxAppNoiseConfig
+
+    class BitBox02PairingDialog(QDialog):
+        def __init__(self, pairing_code: str, device_response: Callable[[], bool]):
+            super(BitBox02PairingDialog, self).__init__()
+            self.ui = Ui_BitBox02PairingDialog()
+            self.ui.setupUi(self)
+            self.setWindowTitle('Verify BitBox02 pairing code')
+            self.ui.pairingCode.setText(pairing_code.replace("\n", "<br>"))
+            self.ui.buttonBox.setEnabled(False)
+            self.device_response = device_response
+            self.painted = False
+
+        def paintEvent(self, ev):
+            super().paintEvent(ev)
+            self.painted = True
+
+        def enable_buttons(self):
+            self.ui.buttonBox.setEnabled(True)
+
+    class BitBox02NoiseConfig(BitBoxAppNoiseConfig):
+        """ GUI elements to perform the BitBox02 pairing and attestatoin check """
+
+        def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:
+            dialog = BitBox02PairingDialog(code, device_response)
+            dialog.show()
+            # render the window since the next operation is blocking
+            while True:
+                QCoreApplication.processEvents()
+                if dialog.painted:
+                    break
+                time.sleep(0.1)
+            if not device_response():
+                return False
+            dialog.enable_buttons()
+            dialog.exec_()
+            return dialog.result() == QDialog.Accepted
+
+        def attestation_check(self, result: bool) -> None:
+            if not result:
+                QMessageBox.warning(
+                    None,
+                    "BitBox02 attestation check",
+                    "BitBox02 attestation check failed. Your BitBox02 might not be genuine. Please contact support@shiftcrypto.ch if the problem persists.",
+                )
+except ImportError:
+    pass
 
 class HWIQt(QMainWindow):
-    def __init__(self, passphrase='', chain=Chain.MAIN):
+    def __init__(self, passphrase=None, chain=Chain.MAIN):
         super(HWIQt, self).__init__()
         self.ui = Ui_MainWindow()
         self.ui.setupUi(self)
@@ -466,7 +473,7 @@ def toggle_passphrase(self):
 
 def process_gui_commands(cli_args):
     parser = HWIArgumentParser(description='Hardware Wallet Interface Qt, version {}.\nInteractively access and send commands to a hardware wallet device with a GUI. Responses are in JSON format.'.format(__version__))
-    parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default='')
+    parser.add_argument('--password', '-p', help='Device password if it has one (e.g. DigitalBitbox)', default=None)
     parser.add_argument('--chain', help='Select chain to work with', type=Chain.argparse, choices=list(Chain), default=Chain.MAIN)
     parser.add_argument('--debug', help='Print debug statements', action='store_true')
     parser.add_argument('--version', action='version', version='%(prog)s {}'.format(__version__))
diff --git a/hwilib/devices/bitbox02.py b/hwilib/devices/bitbox02.py
index c56b1fae6..c2be04817 100644
--- a/hwilib/devices/bitbox02.py
+++ b/hwilib/devices/bitbox02.py
@@ -21,8 +21,7 @@
 import sys
 from functools import wraps
 
-import base58
-
+from .._base58 import decode_check
 from ..descriptor import MultisigDescriptor
 from ..hwwclient import HardwareWalletClient
 from ..key import ExtendedKey
@@ -62,9 +61,9 @@
 
 import hid
 
-from bitbox02 import util
-from bitbox02 import bitbox02
-from bitbox02.communication import (
+from .bitbox02_lib import util
+from .bitbox02_lib import bitbox02
+from .bitbox02_lib.communication import (
     devices,
     u2fhid,
     FirmwareVersionOutdatedException,
@@ -74,7 +73,7 @@
     ERR_GENERIC,
 )
 
-from bitbox02.communication.bitbox_api_protocol import (
+from .bitbox02_lib.communication.bitbox_api_protocol import (
     Platform,
     BitBox02Edition,
     BitBoxNoiseConfig,
@@ -423,7 +422,7 @@ def _multisig_scriptconfig(
             xpubs.append(xpub)
             if device_fingerprint == keyinfo.fingerprint and keyinfo.path:
                 if _xpubs_equal_ignoring_version(
-                    base58.b58decode_check(self._get_xpub(keyinfo.path)), xpub
+                    decode_check(self._get_xpub(keyinfo.path)), xpub
                 ):
                     our_xpub_index = i
                     our_account_keypath = keyinfo.path
@@ -441,7 +440,7 @@ def _multisig_scriptconfig(
                 script_config=bitbox02.btc.BTCScriptConfig(
                     multisig=bitbox02.btc.BTCScriptConfig.Multisig(
                         threshold=threshold,
-                        xpubs=[util.parse_xpub(base58.b58encode_check(xpub).decode()) for xpub in xpubs],
+                        xpubs=[util.parse_xpub(encode_check(xpub).decode()) for xpub in xpubs],
                         our_xpub_index=our_xpub_index,
                         script_type=script_type,
                     )
diff --git a/hwilib/devices/bitbox02_lib/LICENSE b/hwilib/devices/bitbox02_lib/LICENSE
new file mode 100644
index 000000000..6e07d713d
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/LICENSE
@@ -0,0 +1,203 @@
+Copyright 2019 Shift Cryptosecurity AG, Switzerland. All rights reserved.
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/hwilib/devices/bitbox02_lib/README.md b/hwilib/devices/bitbox02_lib/README.md
new file mode 100644
index 000000000..6285ee407
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/README.md
@@ -0,0 +1,12 @@
+# Python BitBox02 Library
+
+This is a slightly modified version of the official [bitbox02](https://github.com/digitalbitbox/bitbox02-firmware/tree/master/py/bitbox02) library.
+
+This was made at commit [5b004e84e2928545db881e380c8ae8782743f5b2](https://github.com/digitalbitbox/bitbox02-firmware/commit/5b004e84e2928545db881e380c8ae8782743f5b2)
+
+## Changes
+
+- Use our own _base58 rather than external base58 library
+- Use relative imports between bitbox02 and communication instead of standard imports that require module installation
+
+See commit ac1d5184f1d7c34630b7eb02d1ce5a7b1e16dc61 for the modifications made
diff --git a/hwilib/devices/bitbox02_lib/__init__.py b/hwilib/devices/bitbox02_lib/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/hwilib/devices/bitbox02_lib/bitbox02/__init__.py b/hwilib/devices/bitbox02_lib/bitbox02/__init__.py
new file mode 100644
index 000000000..046ff978b
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/bitbox02/__init__.py
@@ -0,0 +1,61 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+""" Library to interact with a BitBox02 device. """
+
+from __future__ import print_function
+import sys
+
+__version__ = "6.2.0"
+
+if sys.version_info.major != 3 or sys.version_info.minor < 6:
+    print(
+        "Python version is {}.{}, but 3.6+ is required by this script.".format(
+            sys.version_info.major, sys.version_info.minor
+        ),
+        file=sys.stderr,
+    )
+    sys.exit(1)
+
+try:
+    import hid
+
+    hid.device  # pylint: disable=pointless-statement
+except AttributeError:
+    print(
+        "Unable to reference hid.device; maybe hid package is masking "
+        "hidapi? Try:\n\t$ pip3 uninstall hid",
+        file=sys.stderr,
+    )
+    sys.exit(1)
+
+# pylint: disable=wrong-import-position
+from .bitbox02 import (
+    btc_sign_needs_prevtxs,
+    Backup,
+    BitBox02,
+    BTCInputType,
+    BTCOutputExternal,
+    BTCOutputInternal,
+    BTCOutputType,
+    BTCPrevTxInputType,
+    BTCPrevTxOutputType,
+    DuplicateEntryException,
+    hww,
+    btc,
+    cardano,
+    common,
+    eth,
+    system,
+)
+from .bootloader import Bootloader
diff --git a/hwilib/devices/bitbox02_lib/bitbox02/bitbox02.py b/hwilib/devices/bitbox02_lib/bitbox02/bitbox02.py
new file mode 100644
index 000000000..a297fc206
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/bitbox02/bitbox02.py
@@ -0,0 +1,1093 @@
+# Copyright 2019 Shift Cryptosecurity AG
+# Copyright 2020 Shift Crypto AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""BitBox02"""
+
+import os
+import sys
+import time
+from datetime import datetime
+from typing import Optional, List, Dict, Tuple, Any, Generator, Union, Sequence
+from typing_extensions import TypedDict
+
+import semver
+
+from ..communication import (
+    BitBoxCommonAPI,
+    Bitbox02Exception,
+    ERR_GENERIC,
+    ERR_DUPLICATE_ENTRY,
+)
+
+from .secp256k1 import antiklepto_host_commit, antiklepto_verify
+
+from ..communication.generated import hww_pb2 as hww
+from ..communication.generated import eth_pb2 as eth
+from ..communication.generated import btc_pb2 as btc
+from ..communication.generated import cardano_pb2 as cardano
+from ..communication.generated import mnemonic_pb2 as mnemonic
+from ..communication.generated import bitbox02_system_pb2 as bitbox02_system
+from ..communication.generated import backup_commands_pb2 as backup
+from ..communication.generated import common_pb2 as common
+from ..communication.generated import keystore_pb2 as keystore
+from ..communication.generated import antiklepto_pb2 as antiklepto
+
+# pylint: disable=unused-import
+# We export it in __init__.py
+from ..communication.generated import system_pb2 as system
+
+try:
+    # Optional rlp dependency only needed to sign ethereum transactions.
+    # pylint: disable=import-error
+    import rlp
+except ModuleNotFoundError:
+    pass
+
+HARDENED = 0x80000000
+
+Backup = Tuple[str, str, datetime]
+
+
+class DuplicateEntryException(Exception):
+    pass
+
+
+def is_taproot(script_config: btc.BTCScriptConfigWithKeypath) -> bool:
+    # pylint: disable=no-member
+    return (
+        script_config.script_config.WhichOneof("config") == "simple_type"
+        and script_config.script_config.simple_type == btc.BTCScriptConfig.P2TR
+    )
+
+
+def btc_sign_needs_prevtxs(script_configs: Sequence[btc.BTCScriptConfigWithKeypath]) -> bool:
+    """Returns True if the prev_tx field in BTCInputType needs to be
+    populated before calling btc_sign(). This is the case if there are
+    any non-taproot inputs in the transaction to be signed.
+    """
+    return not all(map(is_taproot, script_configs))
+
+
+class BTCPrevTxInputType(TypedDict):
+    prev_out_hash: bytes
+    prev_out_index: int
+    signature_script: bytes
+    sequence: int
+
+
+class BTCPrevTxOutputType(TypedDict):
+    value: int
+    pubkey_script: bytes
+
+
+class BTCPrevTxType(TypedDict):
+    version: int
+    locktime: int
+    inputs: Sequence[BTCPrevTxInputType]
+    outputs: Sequence[BTCPrevTxOutputType]
+
+
+class BTCInputType(TypedDict):
+    prev_out_hash: bytes
+    prev_out_index: int
+    prev_out_value: int
+    sequence: int
+    keypath: Sequence[int]
+    script_config_index: int
+    # Must be the transaction referenced by prev_out_hash. Can be None if `btc_sign_needs_prevtxs()` returns False.
+    prev_tx: Optional[BTCPrevTxType]
+
+
+class BTCOutputInternal:
+    # TODO: Use NamedTuple, but not playing well with protobuf types.
+
+    def __init__(self, keypath: Sequence[int], value: int, script_config_index: int):
+        """
+        keypath: keypath to the change output.
+        """
+        self.keypath = keypath
+        self.value = value
+        self.script_config_index = script_config_index
+
+
+class BTCOutputExternal:
+    # TODO: Use NamedTuple, but not playing well with protobuf types.
+
+    def __init__(self, output_type: "btc.BTCOutputType.V", output_payload: bytes, value: int):
+        self.type = output_type
+        self.payload = output_payload
+        self.value = value
+
+
+BTCOutputType = Union[BTCOutputInternal, BTCOutputExternal]
+
+
+class BitBox02(BitBoxCommonAPI):
+    """Class to communicate with a BitBox02"""
+
+    # pylint: disable=too-many-public-methods
+
+    def device_info(self) -> Dict[str, Any]:
+        """
+        Returns an object with device information, e.g. name, passphrase status, etc.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        device_info_request = bitbox02_system.DeviceInfoRequest()
+        request.device_info.CopyFrom(device_info_request)
+        response = self._msg_query(request, expected_response="device_info")
+        result = {
+            "name": response.device_info.name,
+            "version": response.device_info.version,
+            "initialized": response.device_info.initialized,
+            "mnemonic_passphrase_enabled": response.device_info.mnemonic_passphrase_enabled,
+            "monotonic_increments_remaining": response.device_info.monotonic_increments_remaining,
+        }
+        if self.version >= semver.VersionInfo(9, 6, 0):
+            result["securechip_model"] = response.device_info.securechip_model
+
+        return result
+
+    def set_device_name(self, device_name: str) -> None:
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.device_name.name = device_name
+        self._msg_query(request, expected_response="success")
+
+    def set_password(self, entropy_size: int = 32) -> bool:
+        """
+        Returns True if the user entered the password correctly (passwords match).
+        Returns False otherwise. Entropy size determines the seed size in bytes; must be 16 or 32.
+        """
+        assert entropy_size in (16, 32)
+        if entropy_size == 16:
+            self._require_atleast(semver.VersionInfo(9, 6, 0))
+
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.set_password.entropy = os.urandom(entropy_size)
+        try:
+            self._msg_query(request, expected_response="success")
+        except Bitbox02Exception as err:
+            if err.code == ERR_GENERIC:
+                return False
+            raise
+        return True
+
+    def create_backup(self) -> bool:
+        """
+        Returns True if the backup was created successfully.
+        Returns False otherwise.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.create_backup.timestamp = int(time.time())
+        request.create_backup.timezone_offset = time.localtime().tm_gmtoff
+        try:
+            self._msg_query(request, expected_response="success")
+        except Bitbox02Exception as err:
+            if err.code == ERR_GENERIC:
+                return False
+            raise
+        return True
+
+    def list_backups(self) -> Generator[Backup, None, None]:
+        """
+        Returns a pair of id and timestamp's strings that identify the backups.
+        """
+        # pylint: disable=no-member
+        self.insert_sdcard()
+        request = hww.Request()
+        request.list_backups.CopyFrom(backup.ListBackupsRequest())
+        response = self._msg_query(request, expected_response="list_backups")
+        for info in response.list_backups.info:
+            utcdate = datetime.utcfromtimestamp(info.timestamp)
+            yield (info.id, info.name, utcdate)
+
+    def restore_backup(self, backup_id: str) -> None:
+        """
+        Sends a restore API call to the BitBox. Raises a Bitbox02Exception on failure.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.restore_backup.id = backup_id
+        request.restore_backup.timestamp = int(time.time())
+        request.restore_backup.timezone_offset = time.localtime().tm_gmtoff
+        self._msg_query(request, expected_response="success")
+
+    def check_backup(self, silent: bool = False) -> Optional[str]:
+        """
+        Sends a check backup API call to the BitBox.
+        Returns the backup ID if the backup was found and can be restored.
+        Otherwise, returns None. If silent is True, the result won't be shown on the device screen.
+        """
+        # pylint: disable=no-member
+        self.insert_sdcard()
+        request = hww.Request()
+        request.check_backup.CopyFrom(backup.CheckBackupRequest(silent=silent))
+        try:
+            response = self._msg_query(request, expected_response="check_backup")
+        except Bitbox02Exception as err:
+            if err.code == ERR_GENERIC:
+                return None
+            raise
+        return response.check_backup.id
+
+    def show_mnemonic(self) -> None:
+        """
+        Returns True if mnemonic was successfully shown and confirmed.
+        Raises a Bitbox02Exception on failure.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.show_mnemonic.CopyFrom(mnemonic.ShowMnemonicRequest())
+        self._msg_query(request, expected_response="success")
+
+    def _btc_msg_query(
+        self, btc_request: btc.BTCRequest, expected_response: Optional[str] = None
+    ) -> btc.BTCResponse:
+        """
+        Same as _msg_query, but one nesting deeper for bitcoin messages.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.btc.CopyFrom(btc_request)
+        btc_response = self._msg_query(request, expected_response="btc").btc
+        if (
+            expected_response is not None
+            and btc_response.WhichOneof("response") != expected_response
+        ):
+            raise Exception(
+                "Unexpected response: {}, expected: {}".format(
+                    btc_response.WhichOneof("response"), expected_response
+                )
+            )
+        return btc_response
+
+    def btc_xpub(
+        self,
+        keypath: Sequence[int],
+        coin: "btc.BTCCoin.V" = btc.BTC,
+        xpub_type: "btc.BTCPubRequest.XPubType.V" = btc.BTCPubRequest.XPUB,
+        display: bool = True,
+    ) -> str:
+        """
+        keypath is a list of child derivation numbers.
+        e.g. m/44'/0'/1' corresponds to [44+HARDENED, 0+HARDENED, 1+HARDENED].
+        """
+        # pylint: disable=no-member,too-many-arguments
+        request = hww.Request()
+        request.btc_pub.CopyFrom(
+            btc.BTCPubRequest(coin=coin, keypath=keypath, xpub_type=xpub_type, display=display)
+        )
+        return self._msg_query(request).pub.pub
+
+    def btc_address(
+        self,
+        keypath: Sequence[int],
+        coin: "btc.BTCCoin.V" = btc.BTC,
+        script_config: btc.BTCScriptConfig = btc.BTCScriptConfig(
+            simple_type=btc.BTCScriptConfig.P2WPKH
+        ),
+        display: bool = True,
+    ) -> str:
+        """
+        keypath is a list of child derivation numbers.
+        e.g. m/44'/0'/1'/5/10 corresponds to [44+HARDENED, 0+HARDENED, 1+HARDENED, 5, 10].
+        """
+        # pylint: disable=no-member,too-many-arguments
+        request = hww.Request()
+        request.btc_pub.CopyFrom(
+            btc.BTCPubRequest(
+                coin=coin, keypath=keypath, script_config=script_config, display=display
+            )
+        )
+        return self._msg_query(request).pub.pub
+
+    def btc_is_script_config_registered(
+        self, coin: "btc.BTCCoin.V", script_config: btc.BTCScriptConfig, keypath: Sequence[int]
+    ) -> bool:
+        """
+        Returns True if the script config / account is already registered.
+        """
+        # pylint: disable=no-member
+        request = btc.BTCRequest()
+        request.is_script_config_registered.CopyFrom(
+            btc.BTCIsScriptConfigRegisteredRequest(
+                registration=btc.BTCScriptConfigRegistration(
+                    coin=coin, script_config=script_config, keypath=keypath
+                )
+            )
+        )
+        return self._btc_msg_query(
+            request, expected_response="is_script_config_registered"
+        ).is_script_config_registered.is_registered
+
+    def btc_register_script_config(
+        self,
+        coin: "btc.BTCCoin.V",
+        script_config: btc.BTCScriptConfig,
+        keypath: Sequence[int],
+        name: str,
+        xpub_type: "btc.BTCRegisterScriptConfigRequest.XPubType.V" = btc.BTCRegisterScriptConfigRequest.XPubType.AUTO_ELECTRUM,
+    ) -> None:
+        """
+        Raises Bitbox02Exception with ERR_USER_ABORT on user abort.
+        If name is the empty string, it will be prompted on the device.
+        """
+        # pylint: disable=no-member,too-many-arguments
+
+        if name == "":
+            # prompt on device only available since v9.3.0
+            self._require_atleast(semver.VersionInfo(9, 3, 0))
+
+        assert len(name) <= 30
+
+        # pylint: disable=no-member
+        request = btc.BTCRequest()
+        request.register_script_config.CopyFrom(
+            btc.BTCRegisterScriptConfigRequest(
+                registration=btc.BTCScriptConfigRegistration(
+                    coin=coin, script_config=script_config, keypath=keypath
+                ),
+                name=name,
+                xpub_type=xpub_type,
+            )
+        )
+        try:
+            self._btc_msg_query(request, expected_response="success")
+        except Bitbox02Exception as err:
+            if err.code == ERR_DUPLICATE_ENTRY:
+                raise DuplicateEntryException(
+                    "A multisig account configuration with this name already exists.\n"
+                    "Choose another name."
+                )
+            raise
+
+    # pylint: disable=too-many-arguments
+    def btc_sign(
+        self,
+        coin: "btc.BTCCoin.V",
+        script_configs: Sequence[btc.BTCScriptConfigWithKeypath],
+        inputs: Sequence[BTCInputType],
+        outputs: Sequence[BTCOutputType],
+        version: int = 1,
+        locktime: int = 0,
+        format_unit: "btc.BTCSignInitRequest.FormatUnit.V" = btc.BTCSignInitRequest.FormatUnit.DEFAULT,
+    ) -> Sequence[Tuple[int, bytes]]:
+        """
+        coin: the first element of all provided keypaths must match the coin:
+        - BTC: 0 + HARDENED
+        - Testnets: 1 + HARDENED
+        - LTC: 2 + HARDENED
+        script_configs: types of all inputs and change outputs. The first element of all provided
+        keypaths must match this type:
+        - SCRIPT_P2PKH: 44 + HARDENED
+        - SCRIPT_P2WPKH_P2SH: 49 + HARDENED
+        - SCRIPT_P2WPKH: 84 + HARDENED
+        - SCRIPT_P2TR: 86 + HARDENED
+        inputs: transaction inputs. The previous transactions of the inputs need to be provided
+          if `btc_sign_needs_prevtxs()` returns True.
+        outputs: transaction outputs. Can be an external output
+        (BTCOutputExternal) or an internal output for change (BTCOutputInternal).
+        version, locktime: reserved for future use.
+        Returns: list of (input index, signature) tuples.
+        Raises Bitbox02Exception with ERR_USER_ABORT on user abort.
+        """
+        # pylint: disable=no-member,too-many-branches,too-many-statements
+
+        # Reserved for future use.
+        assert version in (1, 2)
+
+        if any(map(is_taproot, script_configs)):
+            self._require_atleast(semver.VersionInfo(9, 10, 0))
+
+        supports_antiklepto = self.version >= semver.VersionInfo(9, 4, 0)
+
+        sigs: List[Tuple[int, bytes]] = []
+
+        # Init request
+        request = hww.Request()
+        request.btc_sign_init.CopyFrom(
+            btc.BTCSignInitRequest(
+                coin=coin,
+                script_configs=script_configs,
+                version=version,
+                num_inputs=len(inputs),
+                num_outputs=len(outputs),
+                locktime=locktime,
+                format_unit=format_unit,
+            )
+        )
+        next_response = self._msg_query(request, expected_response="btc_sign_next").btc_sign_next
+
+        is_inputs_pass2 = False
+        while True:
+            if next_response.type == btc.BTCSignNextResponse.INPUT:
+                input_index = next_response.index
+                tx_input = inputs[input_index]
+
+                request = hww.Request()
+                request.btc_sign_input.CopyFrom(
+                    btc.BTCSignInputRequest(
+                        prevOutHash=tx_input["prev_out_hash"],
+                        prevOutIndex=tx_input["prev_out_index"],
+                        prevOutValue=tx_input["prev_out_value"],
+                        sequence=tx_input["sequence"],
+                        keypath=tx_input["keypath"],
+                        script_config_index=tx_input["script_config_index"],
+                    )
+                )
+
+                # Anti-Klepto protocol not supported yet for Schnorr signatures.
+                input_is_schnorr = is_taproot(script_configs[tx_input["script_config_index"]])
+                perform_antiklepto = (
+                    supports_antiklepto and is_inputs_pass2 and not input_is_schnorr
+                )
+
+                if perform_antiklepto:
+                    host_nonce = os.urandom(32)
+                    request.btc_sign_input.host_nonce_commitment.commitment = (
+                        antiklepto_host_commit(host_nonce)
+                    )
+
+                next_response = self._msg_query(
+                    request, expected_response="btc_sign_next"
+                ).btc_sign_next
+
+                if perform_antiklepto:
+                    assert next_response.type == btc.BTCSignNextResponse.HOST_NONCE
+                    assert next_response.HasField("anti_klepto_signer_commitment")
+                    signer_commitment = next_response.anti_klepto_signer_commitment.commitment
+
+                    btc_request = btc.BTCRequest()
+                    btc_request.antiklepto_signature.CopyFrom(
+                        antiklepto.AntiKleptoSignatureRequest(host_nonce=host_nonce)
+                    )
+                    next_response = self._btc_msg_query(
+                        btc_request, expected_response="sign_next"
+                    ).sign_next
+
+                    if self.debug:
+                        print(
+                            f"For input {input_index}, the host contributed the nonce {host_nonce.hex()}"
+                        )
+
+                    assert next_response.has_signature
+                    antiklepto_verify(host_nonce, signer_commitment, next_response.signature)
+
+                    if self.debug:
+                        print(f"Antiklepto nonce verification PASSED for input {input_index}")
+
+                if is_inputs_pass2:
+                    assert next_response.has_signature
+                    sigs.append((input_index, next_response.signature))
+
+                if input_index == len(inputs) - 1:
+                    is_inputs_pass2 = True
+
+            elif next_response.type == btc.BTCSignNextResponse.PREVTX_INIT:
+                prevtx = inputs[next_response.index]["prev_tx"]
+                assert prevtx, "Previous transaction missing"
+                btc_request = btc.BTCRequest()
+                btc_request.prevtx_init.CopyFrom(
+                    btc.BTCPrevTxInitRequest(
+                        version=prevtx["version"],
+                        num_inputs=len(prevtx["inputs"]),
+                        num_outputs=len(prevtx["outputs"]),
+                        locktime=prevtx["locktime"],
+                    )
+                )
+                next_response = self._btc_msg_query(
+                    btc_request, expected_response="sign_next"
+                ).sign_next
+            elif next_response.type == btc.BTCSignNextResponse.PREVTX_INPUT:
+                prevtx = inputs[next_response.index]["prev_tx"]
+                assert prevtx, "Previous transaction missing"
+                prevtx_input = prevtx["inputs"][next_response.prev_index]
+                btc_request = btc.BTCRequest()
+                btc_request.prevtx_input.CopyFrom(
+                    btc.BTCPrevTxInputRequest(
+                        prev_out_hash=prevtx_input["prev_out_hash"],
+                        prev_out_index=prevtx_input["prev_out_index"],
+                        signature_script=prevtx_input["signature_script"],
+                        sequence=prevtx_input["sequence"],
+                    )
+                )
+                next_response = self._btc_msg_query(
+                    btc_request, expected_response="sign_next"
+                ).sign_next
+            elif next_response.type == btc.BTCSignNextResponse.PREVTX_OUTPUT:
+                prevtx = inputs[next_response.index]["prev_tx"]
+                assert prevtx, "Previous transaction missing"
+                prevtx_output = prevtx["outputs"][next_response.prev_index]
+                btc_request = btc.BTCRequest()
+                btc_request.prevtx_output.CopyFrom(
+                    btc.BTCPrevTxOutputRequest(
+                        value=prevtx_output["value"], pubkey_script=prevtx_output["pubkey_script"]
+                    )
+                )
+                next_response = self._btc_msg_query(
+                    btc_request, expected_response="sign_next"
+                ).sign_next
+            elif next_response.type == btc.BTCSignNextResponse.OUTPUT:
+                output_index = next_response.index
+                tx_output = outputs[output_index]
+
+                request = hww.Request()
+                if isinstance(tx_output, BTCOutputInternal):
+                    request.btc_sign_output.CopyFrom(
+                        btc.BTCSignOutputRequest(
+                            ours=True,
+                            value=tx_output.value,
+                            keypath=tx_output.keypath,
+                            script_config_index=tx_output.script_config_index,
+                        )
+                    )
+                elif isinstance(tx_output, BTCOutputExternal):
+                    request.btc_sign_output.CopyFrom(
+                        btc.BTCSignOutputRequest(
+                            ours=False,
+                            type=tx_output.type,
+                            payload=tx_output.payload,
+                            value=tx_output.value,
+                        )
+                    )
+                next_response = self._msg_query(
+                    request, expected_response="btc_sign_next"
+                ).btc_sign_next
+            elif next_response.type == btc.BTCSignNextResponse.DONE:
+                break
+            else:
+                raise Exception("unexpected response")
+        return sigs
+
+    def btc_sign_msg(
+        self, coin: "btc.BTCCoin.V", script_config: btc.BTCScriptConfigWithKeypath, msg: bytes
+    ) -> Tuple[bytes, int, bytes]:
+        """
+        Returns a 64 byte sig, the recoverable id, and a 65 byte signature containing
+        the recid, compatible with Electrum.
+        """
+        # pylint: disable=no-member
+
+        self._require_atleast(semver.VersionInfo(9, 2, 0))
+
+        request = btc.BTCRequest()
+        request.sign_message.CopyFrom(
+            btc.BTCSignMessageRequest(coin=coin, script_config=script_config, msg=msg)
+        )
+
+        supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
+        if supports_antiklepto:
+            host_nonce = os.urandom(32)
+
+            request.sign_message.host_nonce_commitment.commitment = antiklepto_host_commit(
+                host_nonce
+            )
+            signer_commitment = self._btc_msg_query(
+                request, expected_response="antiklepto_signer_commitment"
+            ).antiklepto_signer_commitment.commitment
+
+            request = btc.BTCRequest()
+            request.antiklepto_signature.CopyFrom(
+                antiklepto.AntiKleptoSignatureRequest(host_nonce=host_nonce)
+            )
+
+            signature = self._btc_msg_query(
+                request, expected_response="sign_message"
+            ).sign_message.signature
+            antiklepto_verify(host_nonce, signer_commitment, signature[:64])
+
+            if self.debug:
+                print("Antiklepto nonce verification PASSED")
+
+        else:
+            signature = self._btc_msg_query(
+                request, expected_response="sign_message"
+            ).sign_message.signature
+
+        sig, recid = signature[:64], signature[64]
+
+        # See https://github.com/spesmilo/electrum/blob/84dc181b6e7bb20e88ef6b98fb8925c5f645a765/electrum/ecc.py#L521-L523
+        compressed = 4  # BitBox02 uses only compressed pubkeys
+        electrum_sig65 = bytes([27 + compressed + recid]) + sig
+
+        return (sig, recid, electrum_sig65)
+
+    def check_sdcard(self) -> bool:
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.check_sdcard.CopyFrom(bitbox02_system.CheckSDCardRequest())
+        response = self._msg_query(request, expected_response="check_sdcard")
+        return response.check_sdcard.inserted
+
+    def insert_sdcard(self) -> None:
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.insert_remove_sdcard.CopyFrom(
+            bitbox02_system.InsertRemoveSDCardRequest(
+                action=bitbox02_system.InsertRemoveSDCardRequest.INSERT_CARD
+            )
+        )
+        self._msg_query(request, expected_response="success")
+
+    def remove_sdcard(self) -> None:
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.insert_remove_sdcard.CopyFrom(
+            bitbox02_system.InsertRemoveSDCardRequest(
+                action=bitbox02_system.InsertRemoveSDCardRequest.REMOVE_CARD
+            )
+        )
+        self._msg_query(request, expected_response="success")
+
+    def root_fingerprint(self) -> bytes:
+        """
+        Get the root fingerprint from the bitbox02
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.fingerprint.CopyFrom(common.RootFingerprintRequest())
+        response = self._msg_query(request, expected_response="fingerprint")
+        return response.fingerprint.fingerprint
+
+    def electrum_encryption_key(self, keypath: Sequence[int]) -> str:
+        """
+        This call fetches the xpub used for the electrum wallet encryption
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.electrum_encryption_key.CopyFrom(
+            keystore.ElectrumEncryptionKeyRequest(keypath=keypath)
+        )
+        return self._msg_query(request).electrum_encryption_key.key
+
+    def enable_mnemonic_passphrase(self) -> None:
+        """
+        Enable the bip39 passphrase.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.set_mnemonic_passphrase_enabled.enabled = True
+        self._msg_query(request, expected_response="success")
+
+    def disable_mnemonic_passphrase(self) -> None:
+        """
+        Disable the bip39 passphrase.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.set_mnemonic_passphrase_enabled.enabled = False
+        self._msg_query(request, expected_response="success")
+
+    def _eth_msg_query(
+        self, eth_request: eth.ETHRequest, expected_response: Optional[str] = None
+    ) -> eth.ETHResponse:
+        """
+        Same as _msg_query, but one nesting deeper for ethereum messages.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.eth.CopyFrom(eth_request)
+        eth_response = self._msg_query(request, expected_response="eth").eth
+        if (
+            expected_response is not None
+            and eth_response.WhichOneof("response") != expected_response
+        ):
+            raise Exception(
+                "Unexpected response: {}, expected: {}".format(
+                    eth_response.WhichOneof("response"), expected_response
+                )
+            )
+        return eth_response
+
+    def _eth_coin(self, chain_id: int) -> "eth.ETHCoin.V":
+        """Returns the deprecated `coin` enum value for a given chain_id. Only ETH, Ropsten and Rinkeby are converted, as these were the only supported networks up to v9.10.0. With v9.10.0, the chain ID is passed directly, and the `coin` field is ignored."""
+        if self.version < semver.VersionInfo(9, 10, 0):
+            return {
+                1: eth.ETHCoin.ETH,
+                3: eth.ETHCoin.RopstenETH,
+                4: eth.ETHCoin.RinkebyETH,
+            }[chain_id]
+        return eth.ETHCoin.ETH
+
+    def eth_pub(
+        self,
+        keypath: Sequence[int],
+        chain_id: int = 1,
+        output_type: "eth.ETHPubRequest.OutputType.V" = eth.ETHPubRequest.ADDRESS,
+        display: bool = True,
+        contract_address: bytes = b"",
+    ) -> str:
+        """
+        keypath is a list of child derivation numbers.
+        e.g. m/44'/60'/0'/0/5 corresponds to [44+HARDENED, 60+HARDENED, 0+HARDENED, 0, 5].
+        """
+        # pylint: disable=no-member
+        request = eth.ETHRequest()
+        request.pub.CopyFrom(
+            eth.ETHPubRequest(
+                coin=self._eth_coin(chain_id),
+                chain_id=chain_id,
+                keypath=keypath,
+                output_type=output_type,
+                display=display,
+                contract_address=contract_address,
+            )
+        )
+        return self._eth_msg_query(request, expected_response="pub").pub.pub
+
+    def eth_sign(self, transaction: bytes, keypath: Sequence[int], chain_id: int = 1) -> bytes:
+        """
+        transaction should be given as a full rlp encoded eth transaction.
+        """
+        nonce, gas_price, gas_limit, recipient, value, data, _, _, _ = rlp.decode(transaction)
+        request = eth.ETHRequest()
+        # pylint: disable=no-member
+        request.sign.CopyFrom(
+            eth.ETHSignRequest(
+                coin=self._eth_coin(chain_id),
+                chain_id=chain_id,
+                keypath=keypath,
+                nonce=nonce,
+                gas_price=gas_price,
+                gas_limit=gas_limit,
+                recipient=recipient,
+                value=value,
+                data=data,
+            )
+        )
+
+        supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
+        if supports_antiklepto:
+            host_nonce = os.urandom(32)
+
+            request.sign.host_nonce_commitment.commitment = antiklepto_host_commit(host_nonce)
+            signer_commitment = self._eth_msg_query(
+                request, expected_response="antiklepto_signer_commitment"
+            ).antiklepto_signer_commitment.commitment
+
+            request = eth.ETHRequest()
+            request.antiklepto_signature.CopyFrom(
+                antiklepto.AntiKleptoSignatureRequest(host_nonce=host_nonce)
+            )
+
+            signature = self._eth_msg_query(request, expected_response="sign").sign.signature
+            antiklepto_verify(host_nonce, signer_commitment, signature[:64])
+
+            if self.debug:
+                print("Antiklepto nonce verification PASSED")
+
+            return signature
+
+        return self._eth_msg_query(request, expected_response="sign").sign.signature
+
+    def eth_sign_msg(self, msg: bytes, keypath: Sequence[int], chain_id: int = 1) -> bytes:
+        """
+        Signs message, the msg will be prefixed with "\x19Ethereum message\n" + len(msg) in the
+        hardware. 27 is added to the recID to denote an uncompressed pubkey.
+        """
+
+        def format_as_uncompressed(sig: bytes) -> bytes:
+            # 27 is the magic constant to add to the recoverable ID to denote an uncompressed
+            # pubkey.
+            modified_signature = list(sig)
+            modified_signature[64] += 27
+            return bytes(modified_signature)
+
+        request = eth.ETHRequest()
+        # pylint: disable=no-member
+        request.sign_msg.CopyFrom(
+            eth.ETHSignMessageRequest(
+                coin=self._eth_coin(chain_id), chain_id=chain_id, keypath=keypath, msg=msg
+            )
+        )
+
+        supports_antiklepto = self.version >= semver.VersionInfo(9, 5, 0)
+        if supports_antiklepto:
+            host_nonce = os.urandom(32)
+
+            request.sign_msg.host_nonce_commitment.commitment = antiklepto_host_commit(host_nonce)
+            signer_commitment = self._eth_msg_query(
+                request, expected_response="antiklepto_signer_commitment"
+            ).antiklepto_signer_commitment.commitment
+
+            request = eth.ETHRequest()
+            request.antiklepto_signature.CopyFrom(
+                antiklepto.AntiKleptoSignatureRequest(host_nonce=host_nonce)
+            )
+
+            signature = self._eth_msg_query(request, expected_response="sign").sign.signature
+            antiklepto_verify(host_nonce, signer_commitment, signature[:64])
+
+            if self.debug:
+                print("Antiklepto nonce verification PASSED")
+
+            return format_as_uncompressed(signature)
+
+        signature = self._eth_msg_query(request, expected_response="sign").sign.signature
+        return format_as_uncompressed(signature)
+
+    def eth_sign_typed_msg(
+        self, keypath: Sequence[int], msg: Dict[str, Any], chain_id: int = 1
+    ) -> bytes:
+        """
+        Sign a EIP-712 typed message.
+        """
+        # pylint: disable=too-many-statements
+
+        self._require_atleast(semver.VersionInfo(9, 12, 0))
+
+        def format_as_uncompressed(sig: bytes) -> bytes:
+            # 27 is the magic constant to add to the recoverable ID to denote an uncompressed
+            # pubkey.
+            modified_signature = list(sig)
+            modified_signature[64] += 27
+            return bytes(modified_signature)
+
+        request = eth.ETHRequest()
+
+        host_nonce = os.urandom(32)
+
+        def to_type(typ: str) -> eth.ETHSignTypedMessageRequest.MemberType:
+            # pylint: disable=too-many-return-statements, no-member
+
+            if typ.endswith("]"):
+                rest, size = typ[:-1].rsplit("[", 1)
+                return eth.ETHSignTypedMessageRequest.MemberType(
+                    type=eth.ETHSignTypedMessageRequest.DataType.ARRAY,
+                    size=int(size) if size else 0,
+                    array_type=to_type(rest),
+                )
+            if typ.startswith("bytes"):
+                size = typ[len("bytes") :]
+                return eth.ETHSignTypedMessageRequest.MemberType(
+                    type=eth.ETHSignTypedMessageRequest.DataType.BYTES,
+                    size=int(size) if size else 0,
+                )
+            if typ.startswith("uint"):
+                size = typ[len("uint") :]
+                assert size and int(size) % 8 == 0
+                return eth.ETHSignTypedMessageRequest.MemberType(
+                    type=eth.ETHSignTypedMessageRequest.DataType.UINT,
+                    size=int(size) // 8,
+                )
+            if typ.startswith("int"):
+                size = typ[len("int") :]
+                assert size and int(size) % 8 == 0
+                return eth.ETHSignTypedMessageRequest.MemberType(
+                    type=eth.ETHSignTypedMessageRequest.DataType.INT,
+                    size=int(size) // 8,
+                )
+            if typ == "bool":
+                return eth.ETHSignTypedMessageRequest.MemberType(
+                    type=eth.ETHSignTypedMessageRequest.DataType.BOOL,
+                )
+            if typ == "address":
+                return eth.ETHSignTypedMessageRequest.MemberType(
+                    type=eth.ETHSignTypedMessageRequest.DataType.ADDRESS,
+                )
+            if typ == "string":
+                return eth.ETHSignTypedMessageRequest.MemberType(
+                    type=eth.ETHSignTypedMessageRequest.DataType.STRING,
+                )
+            if typ in msg["types"]:
+                return eth.ETHSignTypedMessageRequest.MemberType(
+                    type=eth.ETHSignTypedMessageRequest.DataType.STRUCT,
+                    struct_name=typ,
+                )
+            raise ValueError("Unrecognized type: {}".format(typ))
+
+        def get_value(
+            root_object: "eth.ETHTypedMessageValueResponse.RootObject.V", path: Sequence[int]
+        ) -> bytes:
+            # pylint: disable=too-many-return-statements, too-many-branches, no-member
+
+            if root_object == eth.ETHTypedMessageValueResponse.RootObject.DOMAIN:
+                value = msg["domain"]
+                typ = to_type("EIP712Domain")
+            elif root_object == eth.ETHTypedMessageValueResponse.RootObject.MESSAGE:
+                value = msg["message"]
+                typ = to_type(msg["primaryType"])
+            else:
+                raise ValueError("Unknown root object: {}".format(root_object))
+
+            for element in path:
+                if typ.type == eth.ETHSignTypedMessageRequest.DataType.STRUCT:
+                    struct_member = msg["types"][typ.struct_name][element]
+                    value = value[struct_member["name"]]
+                    typ = to_type(struct_member["type"])
+                elif typ.type == eth.ETHSignTypedMessageRequest.DataType.ARRAY:
+                    value = value[element]
+                    typ = typ.array_type
+                else:
+                    raise ValueError("Path element does not point to struct or array")
+
+            if typ.type == eth.ETHSignTypedMessageRequest.DataType.BYTES:
+                if isinstance(value, str):
+                    if value == "":
+                        return b""
+                    if value[:2].lower() == "0x":
+                        return bytes.fromhex(value[2:])
+                assert isinstance(value, bytes)
+                return value
+            if typ.type == eth.ETHSignTypedMessageRequest.DataType.UINT:
+                if isinstance(value, str):
+                    value = int(value)
+                assert isinstance(value, int)
+                return value.to_bytes(typ.size, "big")
+            if typ.type == eth.ETHSignTypedMessageRequest.DataType.INT:
+                if isinstance(value, str):
+                    value = int(value)
+                assert isinstance(value, int)
+                return value.to_bytes(typ.size, "big", signed=True)
+            if typ.type == eth.ETHSignTypedMessageRequest.DataType.BOOL:
+                return bytes([value])
+            if typ.type == eth.ETHSignTypedMessageRequest.DataType.ADDRESS:
+                assert isinstance(value, str)
+                return value.encode("ascii")
+            if typ.type == eth.ETHSignTypedMessageRequest.DataType.STRING:
+                assert isinstance(value, str)
+                return value.encode("ascii")
+            if typ.type == eth.ETHSignTypedMessageRequest.DataType.ARRAY:
+                return len(value).to_bytes(4, "big")
+            raise ValueError("Unexpected value query at path: {}. Type={}".format(path, typ))
+
+        # pylint: disable=no-member
+        request.sign_typed_msg.CopyFrom(
+            eth.ETHSignTypedMessageRequest(
+                chain_id=chain_id,
+                keypath=keypath,
+                types=[
+                    eth.ETHSignTypedMessageRequest.StructType(
+                        name=key,
+                        members=[
+                            eth.ETHSignTypedMessageRequest.Member(
+                                name=member["name"],
+                                type=to_type(member["type"]),
+                            )
+                            for member in val
+                        ],
+                    )
+                    for key, val in msg["types"].items()
+                ],
+                primary_type=msg["primaryType"],
+                host_nonce_commitment=antiklepto.AntiKleptoHostNonceCommitment(
+                    commitment=antiklepto_host_commit(host_nonce),
+                ),
+            )
+        )
+
+        response = self._eth_msg_query(request)
+        while response.WhichOneof("response") == "typed_msg_value":
+            response = self._eth_msg_query(
+                eth.ETHRequest(
+                    typed_msg_value=eth.ETHTypedMessageValueRequest(
+                        value=get_value(
+                            response.typed_msg_value.root_object, response.typed_msg_value.path
+                        ),
+                    ),
+                )
+            )
+
+        assert response.WhichOneof("response") == "antiklepto_signer_commitment"
+        signer_commitment = response.antiklepto_signer_commitment.commitment
+
+        request = eth.ETHRequest()
+        request.antiklepto_signature.CopyFrom(
+            antiklepto.AntiKleptoSignatureRequest(host_nonce=host_nonce)
+        )
+
+        signature = self._eth_msg_query(request, expected_response="sign").sign.signature
+        antiklepto_verify(host_nonce, signer_commitment, signature[:64])
+
+        if self.debug:
+            print("Antiklepto nonce verification PASSED")
+
+        return format_as_uncompressed(signature)
+
+    def reset(self) -> bool:
+        """
+        Factory reset the device. Returns True on success.
+        """
+        request = hww.Request()
+        # pylint: disable=no-member
+        request.reset.CopyFrom(bitbox02_system.ResetRequest())
+        try:
+            self._msg_query(request)
+        except OSError:
+            # In case of reboot we can't read the response.
+            return True
+        except Bitbox02Exception as err:
+            if err.code == ERR_GENERIC:
+                return False
+            raise
+        return True
+
+    def restore_from_mnemonic(self) -> None:
+        """
+        Restore from mnemonic. Raises a Bitbox02Exception on failure.
+        """
+        request = hww.Request()
+        # pylint: disable=no-member
+        request.restore_from_mnemonic.CopyFrom(
+            mnemonic.RestoreFromMnemonicRequest(
+                timestamp=int(time.time()), timezone_offset=time.localtime().tm_gmtoff
+            )
+        )
+        self._msg_query(request)
+
+    def _cardano_msg_query(
+        self, cardano_request: cardano.CardanoRequest, expected_response: Optional[str] = None
+    ) -> cardano.CardanoResponse:
+        """
+        Same as _msg_query, but one nesting deeper for cardano messages.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.cardano.CopyFrom(cardano_request)
+        cardano_response = self._msg_query(request, expected_response="cardano").cardano
+        if (
+            expected_response is not None
+            and cardano_response.WhichOneof("response") != expected_response
+        ):
+            raise Exception(
+                "Unexpected response: {}, expected: {}".format(
+                    cardano_response.WhichOneof("response"), expected_response
+                )
+            )
+        return cardano_response
+
+    def cardano_xpubs(self, keypaths: Sequence[Sequence[int]]) -> Sequence[bytes]:
+        request = cardano.CardanoRequest(
+            xpubs=cardano.CardanoXpubsRequest(
+                keypaths=[common.Keypath(keypath=keypath) for keypath in keypaths]
+            )
+        )
+        return self._cardano_msg_query(request, expected_response="xpubs").xpubs.xpubs
+
+    def cardano_address(self, address: cardano.CardanoAddressRequest) -> str:
+        # pylint: disable=no-member
+
+        request = cardano.CardanoRequest(address=address)
+        return self._cardano_msg_query(request, expected_response="pub").pub.pub
+
+    def cardano_sign_transaction(
+        self, transaction: cardano.CardanoSignTransactionRequest
+    ) -> cardano.CardanoSignTransactionResponse:
+        request = cardano.CardanoRequest(sign_transaction=transaction)
+        return self._cardano_msg_query(
+            request, expected_response="sign_transaction"
+        ).sign_transaction
diff --git a/hwilib/devices/bitbox02_lib/bitbox02/bootloader.py b/hwilib/devices/bitbox02_lib/bitbox02/bootloader.py
new file mode 100644
index 000000000..21ddd35a0
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/bitbox02/bootloader.py
@@ -0,0 +1,200 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+""" Interact with a BitBox02 bootloader. """
+
+import struct
+import typing
+import io
+import math
+import hashlib
+
+from ..communication import TransportLayer
+from ..communication.devices import DeviceInfo
+
+BOOTLOADER_CMD = 0x80 + 0x40 + 0x03
+NUM_ROOT_KEYS = 3
+NUM_SIGNING_KEYS = 3
+
+MAX_FIRMWARE_SIZE = 884736  # 928kB - 64kB
+CHUNK_SIZE = 4096
+# max number of chunks that can be written when flashing the firmware
+assert MAX_FIRMWARE_SIZE % CHUNK_SIZE == 0
+FIRMWARE_CHUNKS = MAX_FIRMWARE_SIZE // CHUNK_SIZE
+
+SIGDATA_MAGIC_STANDARD = struct.pack(">I", 0x653F362B)
+SIGDATA_MAGIC_BTCONLY = struct.pack(">I", 0x11233B0B)
+SIGDATA_MAGIC_BITBOXBASE_STANDARD = struct.pack(">I", 0xAB6BD345)
+
+MAGIC_LEN = 4
+
+VERSION_LEN = 4
+SIGNING_PUBKEYS_DATA_LEN = VERSION_LEN + NUM_SIGNING_KEYS * 64 + NUM_ROOT_KEYS * 64
+FIRMWARE_DATA_LEN = VERSION_LEN + NUM_SIGNING_KEYS * 64
+SIGDATA_LEN = SIGNING_PUBKEYS_DATA_LEN + FIRMWARE_DATA_LEN
+
+
+def parse_signed_firmware(firmware: bytes) -> typing.Tuple[bytes, bytes, bytes]:
+    """
+    Split raw firmware bytes into magic, sigdata and firmware
+    """
+
+    if len(firmware) < MAGIC_LEN + SIGDATA_LEN:
+        raise ValueError("firmware too small")
+    magic, firmware = firmware[:MAGIC_LEN], firmware[MAGIC_LEN:]
+    if magic not in (
+        SIGDATA_MAGIC_STANDARD,
+        SIGDATA_MAGIC_BTCONLY,
+        SIGDATA_MAGIC_BITBOXBASE_STANDARD,
+    ):
+        raise ValueError("invalid magic")
+
+    sigdata, firmware = firmware[:SIGDATA_LEN], firmware[SIGDATA_LEN:]
+    return magic, sigdata, firmware
+
+
+class Bootloader:
+    """
+    One instance of a BitBox02 Bootloader, exposing the bootloader API.
+    """
+
+    def __init__(self, transport: TransportLayer, device_info: DeviceInfo):
+        self._transport = transport
+        self.expected_magic = {
+            "bb02-bootloader": SIGDATA_MAGIC_STANDARD,
+            "bb02btc-bootloader": SIGDATA_MAGIC_BTCONLY,
+            "bitboxbase-bootloader": SIGDATA_MAGIC_BITBOXBASE_STANDARD,
+        }.get(device_info["product_string"])
+        assert self.expected_magic
+
+    def _query(self, msg: bytes) -> bytes:
+        cid = self._transport.generate_cid()
+        response = self._transport.query(msg, BOOTLOADER_CMD, cid)
+        if response[0] != msg[0]:
+            raise Exception("bootloader api error, expected {}, got {}".format(msg[0], response[0]))
+        if response[1] != 0:
+            raise Exception("bootloader api error: code={}".format(response[1]))
+        return response[2:]
+
+    def versions(self) -> typing.Tuple[int, int]:
+        """
+        Returns (firmware version, signing pubkeys version).
+        """
+        response = self._query(b"v")
+        firmware_v, signing_pubkeys_v = struct.unpack("<II", response[:8])
+        return firmware_v, signing_pubkeys_v
+
+    def get_hashes(
+        self, display_firmware_hash: bool = False, display_signing_keydata_hash: bool = False
+    ) -> typing.Tuple[bytes, bytes]:
+        """
+        Returns (firmare hash, signing keydata hash).
+        If display is True, the hash is shown on the device screen.
+        """
+        response = self._query(
+            b"h" + bytes([int(display_firmware_hash), int(display_signing_keydata_hash)])
+        )
+        firmware_hash, signing_keydata_hash = response[:32], response[32:64]
+        return firmware_hash, signing_keydata_hash
+
+    def show_firmware_hash_enabled(self) -> bool:
+        """
+        Returns whether the bootloader will automatically show the firmware hash on boot.
+        """
+        return bool(self._query(b"H\xFF")[0])
+
+    def set_show_firmware_hash(self, enable: bool) -> None:
+        """
+        Enables/disables whether the bootloader will automatically show the firmware hash on boot.
+        """
+        self._query(b"H" + bytes([int(enable)]))
+
+    def _erase(self, firmware_num_chunks: int) -> None:
+        self._query(b"e" + bytes([firmware_num_chunks]))
+
+    def _write_chunk(self, chunk_num: int, chunk: bytes) -> None:
+        if len(chunk) != CHUNK_SIZE:
+            raise ValueError("chunk must be 4kB")
+        self._query(b"w" + bytes([chunk_num]) + chunk)
+
+    def flash_unsigned_firmware(
+        self,
+        firmware: bytes,
+        progress_callback: typing.Optional[typing.Callable[[float], None]] = None,
+    ) -> None:
+        """
+        Flashes a firmware image onto the bootloader by invoking the erase and write chunk api
+        calls. Expects the raw firmware without signatures, and does not flash the signatures.
+        """
+        if len(firmware) > FIRMWARE_CHUNKS * CHUNK_SIZE:
+            raise ValueError("firmware too big")
+        if progress_callback is not None:
+            progress_callback(0)
+        num_chunks = math.ceil(len(firmware) / CHUNK_SIZE)
+        self._erase(num_chunks)
+        stream = io.BytesIO(firmware)
+        chunk_num = 0
+        while True:
+            chunk = stream.read(CHUNK_SIZE)
+            if not chunk:
+                break
+            if len(chunk) < CHUNK_SIZE:
+                chunk += b"\xff" * (CHUNK_SIZE - len(chunk))
+            self._write_chunk(chunk_num, chunk)
+            chunk_num += 1
+            if progress_callback is not None:
+                progress_callback(chunk_num / num_chunks)
+
+    def flash_signed_firmware(
+        self,
+        firmware: bytes,
+        progress_callback: typing.Optional[typing.Callable[[float], None]] = None,
+    ) -> None:
+        """
+        Flashes a signed firmware image. The firmware itself is extracted and flashed, then the
+        signatures are extracted and flashed.
+        """
+
+        magic, sigdata, firmware = parse_signed_firmware(firmware)
+        if magic != self.expected_magic:
+            raise ValueError("wrong firmware edition")
+        self.flash_unsigned_firmware(firmware, progress_callback=progress_callback)
+        self._query(b"s" + sigdata)
+
+    def erase(self) -> None:
+        """
+        Erases the firmware from the device.
+        """
+        self._erase(0)
+
+    def erased(self) -> bool:
+        """
+        Returns True if the the device contains no firmware.
+        """
+        # We check by comparing the device reported firmware hash.
+        # If erased, the firmware is all '\xFF'.
+        firmware_v, _ = self.versions()
+        empty_firmware = struct.pack("<I", firmware_v) + b"\xFF" * MAX_FIRMWARE_SIZE
+        empty_firmware_hash = hashlib.sha256(hashlib.sha256(empty_firmware).digest()).digest()
+        reported_firmware_hash, _ = self.get_hashes()
+        return empty_firmware_hash == reported_firmware_hash
+
+    def reboot(self) -> None:
+        self._transport.write(b"r", BOOTLOADER_CMD, self._transport.generate_cid())
+        self._transport.close()
+
+    def screen_rotate(self) -> None:
+        self._query(b"f")
+
+    def close(self) -> None:
+        self._transport.close()
diff --git a/hwilib/devices/bitbox02_lib/bitbox02/py.typed b/hwilib/devices/bitbox02_lib/bitbox02/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/hwilib/devices/bitbox02_lib/bitbox02/secp256k1.py b/hwilib/devices/bitbox02_lib/bitbox02/secp256k1.py
new file mode 100644
index 000000000..a0e471211
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/bitbox02/secp256k1.py
@@ -0,0 +1,58 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""secp256k1 util functions"""
+import hashlib
+
+import ecdsa
+
+
+class ECDSANonceException(Exception):
+    pass
+
+
+def tagged_sha256(tag: bytes, msg: bytes) -> bytes:
+    tag_hash = hashlib.sha256(tag).digest()
+    return hashlib.sha256(tag_hash + tag_hash + msg).digest()
+
+
+def antiklepto_host_commit(host_nonce: bytes) -> bytes:
+    return tagged_sha256(b"s2c/ecdsa/data", host_nonce)
+
+
+def antiklepto_verify(host_nonce: bytes, signer_commitment: bytes, signature: bytes) -> None:
+    """
+    Verifies that hostNonce was used to tweak the nonce during signature
+    generation according to k' = k + H(signerCommitment, hostNonce) by checking that
+    k'*G = signerCommitment + H(signerCommitment, hostNonce)*G.
+    Throws ECDSANonceException if the verification fails.
+    """
+    assert len(host_nonce) == 32
+    assert len(signer_commitment) == 33, "expected compressed pubkey"
+    assert len(signature) == 64
+    signer_commitment_pubkey = ecdsa.VerifyingKey.from_string(
+        signer_commitment, ecdsa.curves.SECP256k1
+    )
+    # Compute R = R1 + H(R1, host_nonce)*G. R1 is the client nonce commitment.
+    tweak = tagged_sha256(b"s2c/ecdsa/point", signer_commitment + host_nonce)
+    tweak_pubkey = ecdsa.SigningKey.from_string(tweak, curve=ecdsa.curves.SECP256k1).verifying_key
+    tweaked_nonce = tweak_pubkey.pubkey.point + signer_commitment_pubkey.pubkey.point
+    expected_sig_r = tweaked_nonce.x() % ecdsa.curves.SECP256k1.order
+    sig_r = int.from_bytes(signature[:32], "big")
+    if sig_r != expected_sig_r:
+        raise ECDSANonceException(
+            "Could not verify that the host nonce was contributed to the signature. "
+            "If this happens repeatedly, the device might be attempting to "
+            "leak the seed through the signature."
+        )
diff --git a/hwilib/devices/bitbox02_lib/communication/__init__.py b/hwilib/devices/bitbox02_lib/communication/__init__.py
new file mode 100644
index 000000000..0649d87de
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/__init__.py
@@ -0,0 +1,30 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Abstraction for the transport layer used for transmitting U2F messages."""
+
+from .communication import PhysicalLayer, TransportLayer
+from .bitbox_api_protocol import (
+    BitBoxNoiseConfig,
+    BitBoxCommonAPI,
+    Bitbox02Exception,
+    UserAbortException,
+    AttestationException,
+    FirmwareVersionOutdatedException,
+    LibraryVersionOutdatedException,
+    ERR_DUPLICATE_ENTRY,
+    ERR_GENERIC,
+    ERR_USER_ABORT,
+    HARDENED,
+)
diff --git a/hwilib/devices/bitbox02_lib/communication/bitbox_api_protocol.py b/hwilib/devices/bitbox02_lib/communication/bitbox_api_protocol.py
new file mode 100644
index 000000000..b12974657
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/bitbox_api_protocol.py
@@ -0,0 +1,736 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""BitBox02"""
+
+from abc import ABC, abstractmethod
+import os
+import enum
+import sys
+import base64
+import binascii
+import hashlib
+import time
+from typing import Callable, Optional, Dict, Tuple, Union, Sequence
+from typing_extensions import TypedDict
+
+import ecdsa
+from noise.connection import NoiseConnection, Keypair
+import semver
+
+from .devices import parse_device_version, DeviceInfo
+
+from .communication import TransportLayer
+from .devices import BITBOX02MULTI, BITBOX02BTC
+
+from .generated import hww_pb2 as hww
+from .generated import system_pb2 as system
+
+
+HWW_CMD = 0x80 + 0x40 + 0x01
+
+
+class HwwRequestCode:
+    # New request.
+    REQ_NEW = b"\x00"
+    # Poll an outstanding request for completion.
+    REQ_RETRY = b"\x01"
+    # Cancel any outstanding request.
+    REQ_CANCEL = b"\x02"
+    # INFO api call (used to be OP_INFO api call), graduated to the toplevel framing so it works
+    # the same way for all firmware versions.
+    REQ_INFO = b"i"
+
+
+class HwwResponseCode:
+    # Request finished, payload is valid.
+    RSP_ACK = b"\x00"
+    # Request is outstanding, retry later.
+    RSP_NOT_READY = b"\x01"
+    # Device is busy, request was dropped.
+    RSP_BUSY = b"\x02"
+    # Bad request.
+    RSP_NACK = b"\x03"
+
+
+ERR_GENERIC = 103
+ERR_DUPLICATE_ENTRY = 107
+ERR_USER_ABORT = 104
+
+HARDENED = 0x80000000
+
+
+class AttestationPubkeyInfo(TypedDict):
+    # uncompressed secp256k1 pubkey serialization
+    pubkey: bytes
+    # if not None, a hex-encoded bootloader hashes (of the padded
+    # bootloader binary, i.e. the device bootloader area), for which
+    # this attestation pubkey is
+    accepted_bootloader_hash: Optional[bytes]
+
+
+ATTESTATION_PUBKEYS: Sequence[AttestationPubkeyInfo] = [
+    {
+        "pubkey": binascii.unhexlify(
+            "04074ff1273b36c24e80fe3d59e0e897a81732d3f8e9cd07e17e9fc06319cd16b"
+            "25cf74255674477b3ac9cbac2d12f0dc27a662681fcbc12955b0bccdcbbdcfd01"
+        ),
+        "accepted_bootloader_hash": None,
+    },
+    {
+        "pubkey": binascii.unhexlify(
+            "044c53a84f41fa7301b378bb3c260fc9b2ff1cbea7a78181279a8566797a736f1"
+            "2cea25fa2b1c27a844392fe9b37547dc6fbd00a2676b816e7d2d3562be2a0cbbd"
+        ),
+        "accepted_bootloader_hash": None,
+    },
+    {
+        "pubkey": binascii.unhexlify(
+            "04e9c8dc929796aac65af5084eb54dc1ee482d5e0b5c58e2c93f243c5b70b2152"
+            "3324bdb78d7395317da165ef1138826c3ca3c91ca95e6f490c340cf5508a4a3ec"
+        ),
+        "accepted_bootloader_hash": None,
+    },
+    {
+        "pubkey": binascii.unhexlify(
+            "04c2fb05889b9dff5a9fb22a59ee1d16bfc2863f0400ddcb69566e2abe8a15fa0"
+            "ba1240254ca45aa310d170e724e1310ce5f611cada76c12e3c24a926a390ca4be"
+        ),
+        "accepted_bootloader_hash": None,
+    },
+    {
+        "pubkey": binascii.unhexlify(
+            "04c4e82d6d1b91e7853eba96a871ad31fc62620b826b0b8acf815c03de31b792a"
+            "98e05bb34d3b9e0df1040eac485f03ff8bbbf7a857ef1cf2a49a60ac084efb88f"
+        ),
+        "accepted_bootloader_hash": None,
+    },
+    {
+        "pubkey": binascii.unhexlify(
+            "040526f5b8348a8d55e7b1cac043ce98c55bbdb3311b4d1bb2d654281edf8aeb2"
+            "1f018fb027a6b08e4ddc62c919e648690722d00c6f54c668c9bd8224a1d82423a"
+        ),
+        "accepted_bootloader_hash": binascii.unhexlify(
+            "e8fa0bd5fc80b86b9f1ea983664df33b27f6f95855d79fb43248ee4c3d3e6be6"
+        ),
+    },
+    {
+        "pubkey": binascii.unhexlify(
+            "0422491e19766bd96a56e3f2f3926a6c57b89209ff47bd10e523b223ff65ab9af"
+            "11c0a5f62c187514f2117ce772de90f9901ee122af78e69bbc4d29eec811be8ec"
+        ),
+        "accepted_bootloader_hash": None,
+    },
+    {
+        "pubkey": binascii.unhexlify(
+            "049f1b7180014b6de60d41f16a3c0a37b20146585e4884960249d30f3cd68c74d"
+            "04420d0cedef5719d6b1529b085ecd534fa6c1690be5eb1b3331bc57b5db224dc"
+        ),
+        "accepted_bootloader_hash": None,
+    },
+    {
+        "pubkey": binascii.unhexlify(
+            "04adaa011a4ced11310728abb64f09636267ce0b05782da6d3eeaf987cec7c64f"
+            "279ad55327184f9e5b4a1e53089b31bcc65032dad7205325f41ed3d9fdfba1f88"
+        ),
+        "accepted_bootloader_hash": None,
+    },
+    {
+        "pubkey": binascii.unhexlify(
+            "044a70e663d7fe5fe0d4cbbb752883e35222b8d7d7bffdaa8d591995d1252528a"
+            "4e9a3e4d5220d485021728b3cdad4fccc681a6ddeea8e2f7c55b4acde8d53573d"
+        ),
+        "accepted_bootloader_hash": None,
+    },
+]
+
+ATTESTATION_PUBKEYS_MAP: Dict[bytes, AttestationPubkeyInfo] = {
+    hashlib.sha256(val["pubkey"]).digest(): val for val in ATTESTATION_PUBKEYS
+}
+
+OP_ATTESTATION = b"a"
+OP_UNLOCK = b"u"
+OP_I_CAN_HAS_HANDSHAEK = b"h"
+OP_HER_COMEZ_TEH_HANDSHAEK = b"H"
+OP_I_CAN_HAS_PAIRIN_VERIFICASHUN = b"v"
+OP_NOISE_MSG = b"n"
+
+RESPONSE_SUCCESS = b"\x00"
+RESPONSE_FAILURE = b"\x01"
+
+MIN_SUPPORTED_BITBOX02_MULTI_FIRMWARE_VERSION = semver.VersionInfo(9, 0, 0)
+MIN_SUPPORTED_BITBOX02_BTCONLY_FIRMWARE_VERSION = semver.VersionInfo(9, 0, 0)
+MIN_UNSUPPORTED_BITBOX02_MULTI_FIRMWARE_VERSION = semver.VersionInfo(10, 0, 0)
+MIN_UNSUPPORTED_BITBOX02_BTCONLY_FIRMWARE_VERSION = semver.VersionInfo(10, 0, 0)
+
+
+class Platform(enum.Enum):
+    """Available hardware platforms"""
+
+    BITBOX02 = "bitbox02"
+
+
+class BitBox02Edition(enum.Enum):
+    """Editions for the BitBox02 platform"""
+
+    MULTI = "multi"
+    BTCONLY = "btconly"
+
+
+class Bitbox02Exception(Exception):
+    def __init__(self, code: int, message: str):
+        self.code = code
+        self.message = message
+        super().__init__()
+
+    def __str__(self) -> str:
+        return f"error code: {self.code}, message: {self.message}"
+
+
+class UserAbortException(Bitbox02Exception):
+    pass
+
+
+class AttestationException(Exception):
+    pass
+
+
+class FirmwareVersionOutdatedException(Exception):
+    def __init__(self, version: semver.VersionInfo, required_version: semver.VersionInfo):
+        super().__init__(
+            "The BitBox02's firmware is not up to date. Device: {}, Required: {}".format(
+                version, required_version
+            )
+        )
+
+
+class LibraryVersionOutdatedException(Exception):
+    def __init__(self, version: semver.VersionInfo):
+        super().__init__(
+            "The BitBox02's firmware version {} is too new for this app. Update the app".format(
+                version
+            )
+        )
+
+
+class UnsupportedException(Exception):
+    def __init__(self, need_atleast: semver.VersionInfo):
+        super().__init__(
+            "This feature is supported from firmware version {}. Please upgrade your firmware.".format(
+                need_atleast
+            )
+        )
+
+
+class BitBoxNoiseConfig:
+    """Stores Functions required setup a noise connection"""
+
+    # pylint: disable=no-self-use,unused-argument
+    def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:
+        """
+        Returns True if the user confirms the pairing (both device and host).
+        Returns False if the user rejects the pairing (either device or host).
+        This function must call device_response() to invoke the pairing screen on the device.
+        """
+        return device_response()
+
+    def attestation_check(self, result: bool) -> None:
+        return
+
+    def contains_device_static_pubkey(self, pubkey: bytes) -> bool:
+        return False
+
+    def add_device_static_pubkey(self, pubkey: bytes) -> None:
+        pass
+
+    def get_app_static_privkey(self) -> Optional[bytes]:
+        return None
+
+    def set_app_static_privkey(self, privkey: bytes) -> None:
+        pass
+
+
+class BitBoxProtocol(ABC):
+    """
+    Class for executing versioned BitBox operations
+    (noise message transmissions, unlocks, etc).
+    """
+
+    def __init__(self, transport: TransportLayer):
+        super().__init__()
+        self._transport = transport
+        self._noise: NoiseConnection = None
+
+    def close(self) -> None:
+        self._transport.close()
+
+    def _raw_query(self, msg: bytes) -> bytes:
+        cid = self._transport.generate_cid()
+        return self._transport.query(msg, HWW_CMD, cid)
+
+    def query(self, cmd: bytes, msg_data: bytes) -> Tuple[bytes, bytes]:
+        """
+        Encapsulates the given OP_* command and message data into a packet,
+        and unpacks the response status code and data.
+        """
+        response = self._raw_query(cmd + msg_data)
+        return response[:1], response[1:]
+
+    @abstractmethod
+    def _encode_noise_request(self, encrypted_msg: bytes) -> bytes:
+        """Encapsulates an OP_NOISE_MSG message."""
+
+    @abstractmethod
+    def _decode_noise_response(self, encrypted_msg: bytes) -> Tuple[bytes, bytes]:
+        """De-encapsulate an OP_NOISE_MSG response."""
+
+    @abstractmethod
+    def _handshake_query(self, req: bytes) -> Tuple[bytes, bytes]:
+        """
+        Executes a OP_HER_COMEZ_TEH_HANDSHAEK query with the given
+        request data.
+        Returns a pair (response status, response data).
+        """
+
+    def encrypted_query(self, msg: bytes) -> bytes:
+        """
+        Sends msg bytes and reads response bytes over an encrypted channel.
+        """
+        encrypted_msg = self._noise.encrypt(msg)
+        encrypted_msg = self._encode_noise_request(encrypted_msg)
+
+        response = self._raw_query(encrypted_msg)
+        response_status, response = self._decode_noise_response(response)
+        if response_status != RESPONSE_SUCCESS:
+            raise Exception("Noise communication failed.")
+
+        result = self._noise.decrypt(response)
+        assert isinstance(result, bytes)
+        return result
+
+    # pylint: disable=too-many-branches
+    def _create_noise_channel(self, noise_config: BitBoxNoiseConfig) -> NoiseConnection:
+        if self._raw_query(OP_I_CAN_HAS_HANDSHAEK) != RESPONSE_SUCCESS:
+            self.close()
+            raise Exception("Couldn't kick off handshake")
+
+        # init noise channel
+        noise = NoiseConnection.from_name(b"Noise_XX_25519_ChaChaPoly_SHA256")
+        noise.set_as_initiator()
+        private_key = noise_config.get_app_static_privkey()
+        if private_key is None:
+            private_key = os.urandom(32)
+            noise_config.set_app_static_privkey(private_key)
+        noise.set_keypair_from_private_bytes(Keypair.STATIC, private_key)
+        noise.set_prologue(b"Noise_XX_25519_ChaChaPoly_SHA256")
+        noise.start_handshake()
+        start_handshake_status, start_handshake_reply = self._handshake_query(noise.write_message())
+        if start_handshake_status != RESPONSE_SUCCESS:
+            self.close()
+            raise Exception("Handshake process request failed.")
+        noise.read_message(start_handshake_reply)
+        remote_static_key = noise.noise_protocol.handshake_state.rs.public_bytes
+        assert not noise.handshake_finished
+        send_msg = noise.write_message()
+        assert noise.handshake_finished
+        pairing_code = base64.b32encode(noise.get_handshake_hash()).decode("ascii")
+        handshake_finish_status, response = self._handshake_query(send_msg)
+        if handshake_finish_status != RESPONSE_SUCCESS:
+            self.close()
+            raise Exception("Handshake conclusion failed.")
+
+        # Check if we recognize the device's public key
+        pairing_verification_required_by_host = True
+        if noise_config.contains_device_static_pubkey(remote_static_key):
+            pairing_verification_required_by_host = False
+
+        pairing_verification_required_by_device = response == b"\x01"
+        if pairing_verification_required_by_host or pairing_verification_required_by_device:
+
+            def get_device_response() -> bool:
+                device_response = self._raw_query(OP_I_CAN_HAS_PAIRIN_VERIFICASHUN)
+                if device_response == RESPONSE_SUCCESS:
+                    return True
+                if device_response == RESPONSE_FAILURE:
+                    return False
+                raise Exception(f"Unexpected pairing response: f{repr(device_response)}")
+
+            client_response_success = noise_config.show_pairing(
+                "{} {}\n{} {}".format(
+                    pairing_code[:5], pairing_code[5:10], pairing_code[10:15], pairing_code[15:20]
+                ),
+                get_device_response,
+            )
+            if not client_response_success:
+                self.close()
+                raise Exception("pairing rejected by the user")
+
+            noise_config.add_device_static_pubkey(remote_static_key)
+        return noise
+
+    def noise_connect(self, noise_config: BitBoxNoiseConfig) -> None:
+        self._noise = self._create_noise_channel(noise_config)
+
+    @abstractmethod
+    def unlock_query(self) -> None:
+        """
+        Executes an unlock query.
+        Returns the bytes containing the response status.
+        """
+
+    @abstractmethod
+    def cancel_outstanding_request(self) -> None:
+        """
+        Aborts/force close the outstanding request on the device.
+        """
+
+
+class BitBoxProtocolV1(BitBoxProtocol):
+    """BitBox Protocol from firmware V1.0.0 onwards."""
+
+    def unlock_query(self) -> None:
+        raise NotImplementedError("unlock_query is not supported in BitBox protocol V1")
+
+    def _encode_noise_request(self, encrypted_msg: bytes) -> bytes:
+        return encrypted_msg
+
+    def _decode_noise_response(self, encrypted_msg: bytes) -> Tuple[bytes, bytes]:
+        """
+        Until V7 of the protocol, we don't encapsulate OP_NOISE_MSG responses.
+        Let's assume that if a response is empty, that means it
+        contains an error.
+        """
+        if len(encrypted_msg) == 0:
+            return RESPONSE_FAILURE, b""
+        return RESPONSE_SUCCESS, encrypted_msg
+
+    def _handshake_query(self, req: bytes) -> Tuple[bytes, bytes]:
+        """
+        V1-6 of the BB noise protocol doesn't encapsulate handshake requests, and don't
+        send back a status code in the response.
+        """
+        noise_result = self._raw_query(req)
+        return RESPONSE_SUCCESS, noise_result
+
+    def cancel_outstanding_request(self) -> None:
+        raise RuntimeError("cancel_outstanding_request should never be called here.")
+
+
+class BitBoxProtocolV2(BitBoxProtocolV1):
+    """BitBox Protocol from firmware V2.0.0 onwards."""
+
+    def unlock_query(self) -> None:
+        unlock_data = self._raw_query(OP_UNLOCK)
+        if len(unlock_data) != 0:
+            raise ValueError("OP_UNLOCK (V2) replied with wrong length.")
+
+
+class BitBoxProtocolV3(BitBoxProtocolV2):
+    """BitBox Protocol from firmware V3.0.0 onwards."""
+
+    def unlock_query(self) -> None:
+        unlock_result, unlock_data = self.query(OP_UNLOCK, b"")
+        if len(unlock_data) != 0:
+            raise ValueError("OP_UNLOCK (V3) replied with wrong length.")
+        if unlock_result == RESPONSE_FAILURE:
+            self.close()
+            raise Exception("Unlock process aborted")
+
+
+class BitBoxProtocolV4(BitBoxProtocolV3):
+    """BitBox Protocol from firmware V4.0.0 onwards."""
+
+    def _encode_noise_request(self, encrypted_msg: bytes) -> bytes:
+        return OP_NOISE_MSG + encrypted_msg
+
+
+class BitBoxProtocolV7(BitBoxProtocolV4):
+    """Noise Protocol from firmware V7.0.0 onwards."""
+
+    def __init__(self, transport: TransportLayer):
+        super().__init__(transport)
+        self.cancel_requested = False
+
+    def _handshake_query(self, req: bytes) -> Tuple[bytes, bytes]:
+        return self.query(OP_HER_COMEZ_TEH_HANDSHAEK, req)
+
+    def _decode_noise_response(self, encrypted_msg: bytes) -> Tuple[bytes, bytes]:
+        return encrypted_msg[:1], encrypted_msg[1:]
+
+    def _raw_query(self, msg: bytes) -> bytes:
+        """
+        Starting with v7.0.0, HWW messages are encapsulated
+        into an arbitration layer. The device can respond with
+        RSP_NOT_READY to indicate that we should poll it later.
+        """
+        cid = self._transport.generate_cid()
+        status = None
+        payload: bytes
+        while True:
+            response = self._transport.query(HwwRequestCode.REQ_NEW + msg, HWW_CMD, cid)
+            assert len(response) != 0, "Unexpected response of length 0 from HWW stack."
+            status, payload = response[:1], response[1:]
+            if status == HwwResponseCode.RSP_BUSY:
+                assert (
+                    len(payload) == 0
+                ), "Unexpected payload of length {} with RSP_BUSY response.".format(len(payload))
+                time.sleep(1)
+            else:
+                # We've successfully initiated our request.
+                break
+
+        if status in [HwwResponseCode.RSP_NACK]:
+            # We should never receive a NACK unless some internal error occurs.
+            raise Exception("Unexpected NACK response from HWW stack.")
+
+        # The message has been sent. If we have a retry, poll the device until we're ready.
+        self.cancel_requested = False
+        while status == HwwResponseCode.RSP_NOT_READY:
+            assert (
+                len(payload) == 0
+            ), "Unexpected payload of length {} with RSP_NOT_READY response.".format(len(payload))
+            time.sleep(0.2)
+            to_send = (
+                HwwRequestCode.REQ_CANCEL if self.cancel_requested else HwwRequestCode.REQ_RETRY
+            )
+            response = self._transport.query(to_send, HWW_CMD, cid)
+            assert len(response) != 0, "Unexpected response of length 0 from HWW stack."
+            status, payload = response[:1], response[1:]
+            if status not in [HwwResponseCode.RSP_NOT_READY, HwwResponseCode.RSP_ACK]:
+                # We should never receive a NACK unless some internal error occurs.
+                raise Exception(
+                    "Unexpected response from HWW stack during retry ({}).".format(repr(status))
+                )
+        return payload
+
+    def cancel_outstanding_request(self) -> None:
+        self.cancel_requested = True
+
+
+class BitBoxCommonAPI:
+    """Class to communicate with a BitBox device"""
+
+    # pylint: disable=too-many-public-methods,too-many-arguments
+    def __init__(
+        self, transport: TransportLayer, device_info: DeviceInfo, noise_config: BitBoxNoiseConfig
+    ):
+        """
+        Can raise LibraryVersionOutdatedException. check_min_version() should be called following
+        the instantiation.
+        """
+        self.debug = False
+        serial_number = device_info["serial_number"]
+
+        if device_info["product_string"] == BITBOX02MULTI:
+            self.edition = BitBox02Edition.MULTI
+        elif device_info["product_string"] == BITBOX02BTC:
+            self.edition = BitBox02Edition.BTCONLY
+
+        self.version = parse_device_version(serial_number)
+        if self.version is None:
+            transport.close()
+            raise ValueError(f"Could not parse version from {serial_number}")
+
+        # Delete the prelease part, as it messes with the comparison (e.g. 3.0.0-pre < 3.0.0 is
+        # True, but the 3.0.0-pre has already the same API breaking changes like 3.0.0...).
+        self.version = semver.VersionInfo(
+            self.version.major, self.version.minor, self.version.patch, build=self.version.build
+        )
+
+        # raises exceptions if the library is out of date
+        self._check_max_version()
+
+        self._bitbox_protocol: BitBoxProtocol
+        if self.version >= semver.VersionInfo(7, 0, 0):
+            self._bitbox_protocol = BitBoxProtocolV7(transport)
+        elif self.version >= semver.VersionInfo(4, 0, 0):
+            self._bitbox_protocol = BitBoxProtocolV4(transport)
+        elif self.version >= semver.VersionInfo(3, 0, 0):
+            self._bitbox_protocol = BitBoxProtocolV3(transport)
+        elif self.version >= semver.VersionInfo(2, 0, 0):
+            self._bitbox_protocol = BitBoxProtocolV2(transport)
+        else:
+            self._bitbox_protocol = BitBoxProtocolV1(transport)
+
+        if self.version >= semver.VersionInfo(2, 0, 0):
+            noise_config.attestation_check(self._perform_attestation())
+            self._bitbox_protocol.unlock_query()
+
+        self._bitbox_protocol.noise_connect(noise_config)
+
+    # pylint: disable=too-many-return-statements
+    def _perform_attestation(self) -> bool:
+        """Sends a random challenge and verifies that the response can be verified with
+        Shift's root attestation pubkeys. Returns True if the verification is successful."""
+
+        challenge = os.urandom(32)
+        response_status, response = self._bitbox_protocol.query(OP_ATTESTATION, challenge)
+        if response_status != RESPONSE_SUCCESS:
+            return False
+
+        # parse data
+        bootloader_hash, response = response[:32], response[32:]
+        device_pubkey_bytes, response = response[:64], response[64:]
+        certificate, response = response[:64], response[64:]
+        root_pubkey_identifier, response = response[:32], response[32:]
+        challenge_signature, response = response[:64], response[64:]
+
+        # check attestation
+        if root_pubkey_identifier not in ATTESTATION_PUBKEYS_MAP:
+            # root pubkey could not be identified.
+            return False
+
+        root_pubkey_info = ATTESTATION_PUBKEYS_MAP[root_pubkey_identifier]
+        root_pubkey_bytes_uncompressed = root_pubkey_info["pubkey"]
+        if (
+            root_pubkey_info["accepted_bootloader_hash"] is not None
+            and root_pubkey_info["accepted_bootloader_hash"] != bootloader_hash
+        ):
+            return False
+
+        root_pubkey = ecdsa.VerifyingKey.from_string(
+            root_pubkey_bytes_uncompressed[1:], ecdsa.curves.SECP256k1
+        )
+
+        device_pubkey = ecdsa.VerifyingKey.from_string(device_pubkey_bytes, ecdsa.curves.NIST256p)
+
+        try:
+            # Verify certificate
+            if not root_pubkey.verify(
+                certificate, bootloader_hash + device_pubkey_bytes, hashfunc=hashlib.sha256
+            ):
+                return False
+
+            # Verify challenge
+            if not device_pubkey.verify(challenge_signature, challenge, hashfunc=hashlib.sha256):
+                return False
+        except ecdsa.BadSignatureError:
+            return False
+        return True
+
+    def _msg_query(
+        self, request: hww.Request, expected_response: Optional[str] = None
+    ) -> hww.Response:
+        """
+        Sends protobuf msg and retrieves protobuf response over an encrypted
+        channel.
+        """
+        # pylint: disable=no-member
+        if self.debug:
+            print(request)
+        response_bytes = self._bitbox_protocol.encrypted_query(request.SerializeToString())
+        response = hww.Response()
+        response.ParseFromString(response_bytes)
+        if response.WhichOneof("response") == "error":
+            if response.error.code == ERR_USER_ABORT:
+                raise UserAbortException(response.error.code, response.error.message)
+            raise Bitbox02Exception(response.error.code, response.error.message)
+        if expected_response is not None and response.WhichOneof("response") != expected_response:
+            raise Exception(
+                "Unexpected response: {}, expected: {}".format(
+                    response.WhichOneof("response"), expected_response
+                )
+            )
+        if self.debug:
+            print(response)
+        return response
+
+    def reboot(
+        self, purpose: "system.RebootRequest.Purpose.V" = system.RebootRequest.Purpose.UPGRADE
+    ) -> bool:
+        """
+        Sends the reboot request. If the user confirms the request on the device, the device reboots
+        into the bootloader.
+        The purpose defines what confirmation message the user gets to see on the device.
+        """
+        # pylint: disable=no-member
+        request = hww.Request()
+        request.reboot.CopyFrom(system.RebootRequest(purpose=purpose))
+        try:
+            self._msg_query(request)
+        except OSError:
+            # In case of reboot we can't read the response.
+            return True
+        except Bitbox02Exception:
+            return False
+        return True
+
+    @staticmethod
+    def get_info(transport: TransportLayer) -> Tuple[str, Platform, Union[BitBox02Edition], bool]:
+        """
+        Returns (version, platform, edition, unlocked).
+        This is useful to get the version of the firmware when a usb descriptor is not available
+        (via BitBoxBridge, etc.).
+        This call does not use a versioned BitBoxProtocol for communication, as the version is not
+        available (this call is used to get the version), so it must work for all firmware versions.
+        """
+        response = transport.query(HwwRequestCode.REQ_INFO, HWW_CMD, transport.generate_cid())
+
+        version_str_len, response = int(response[0]), response[1:]
+        version, response = response[:version_str_len], response[version_str_len:]
+        version_str = version.rstrip(b"\0").decode("ascii")
+
+        platform_byte, response = response[0], response[1:]
+        platform = {0x00: Platform.BITBOX02}[platform_byte]
+
+        edition_byte, response = response[0], response[1:]
+        edition: Union[BitBox02Edition]
+        if platform == Platform.BITBOX02:
+            edition = {0x00: BitBox02Edition.MULTI, 0x01: BitBox02Edition.BTCONLY}[edition_byte]
+        else:
+            raise Exception("Unknown platform: {}".format(platform))
+
+        unlocked_byte = response[0]
+        unlocked = {0x00: False, 0x01: True}[unlocked_byte]
+        return (version_str, platform, edition, unlocked)
+
+    def check_min_version(self) -> None:
+        """
+        Raises FirmwareVersionOutdatedException if the device has an older firmware version than
+        required and the minimum required version.
+        """
+        if self.edition == BitBox02Edition.MULTI:
+            if self.version < MIN_SUPPORTED_BITBOX02_MULTI_FIRMWARE_VERSION:
+                raise FirmwareVersionOutdatedException(
+                    self.version, MIN_SUPPORTED_BITBOX02_MULTI_FIRMWARE_VERSION
+                )
+        elif self.edition == BitBox02Edition.BTCONLY:
+            if self.version < MIN_SUPPORTED_BITBOX02_BTCONLY_FIRMWARE_VERSION:
+                raise FirmwareVersionOutdatedException(
+                    self.version, MIN_SUPPORTED_BITBOX02_BTCONLY_FIRMWARE_VERSION
+                )
+
+    def cancel_outstanding_request(self) -> None:
+        self._bitbox_protocol.cancel_outstanding_request()
+
+    def _check_max_version(self) -> None:
+        """
+        Raises LibraryVersionOutdatedException if the device has an firmware which is too new
+        (major version increased).
+        """
+        if self.edition == BitBox02Edition.MULTI:
+            if self.version >= MIN_UNSUPPORTED_BITBOX02_MULTI_FIRMWARE_VERSION:
+                raise LibraryVersionOutdatedException(self.version)
+        elif self.edition == BitBox02Edition.BTCONLY:
+            if self.version >= MIN_UNSUPPORTED_BITBOX02_BTCONLY_FIRMWARE_VERSION:
+                raise LibraryVersionOutdatedException(self.version)
+
+    def _require_atleast(self, version: semver.VersionInfo) -> None:
+        """
+        Raises UnsupportedException if the current firmware version is not at least the required version.
+        """
+        if self.version < version:
+            raise UnsupportedException(version)
+
+    def close(self) -> None:
+        self._bitbox_protocol.close()
diff --git a/hwilib/devices/bitbox02_lib/communication/communication.py b/hwilib/devices/bitbox02_lib/communication/communication.py
new file mode 100644
index 000000000..6dea22107
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/communication.py
@@ -0,0 +1,56 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""
+Common interfaces to be used for communication with BitBox devices.
+"""
+
+from typing_extensions import Protocol
+
+
+class TransportLayer(Protocol):
+    """
+    Abstraction for the transport layer used for transmitting U2F messages.
+    This class encapsulates packets on a given physical link capable of
+    transmitting byte strings.
+    """
+
+    # pylint: disable=unused-argument,no-self-use
+    def write(self, data: bytes, endpoint: int, cid: int) -> None:
+        """Sends a frame of data to the specified endpoint"""
+
+    def read(self, endpoint: int, cid: int) -> bytes:
+        ...
+
+    def query(self, data: bytes, endpoint: int, cid: int) -> bytes:
+        self.write(data, endpoint, cid)
+        return self.read(endpoint, cid)
+
+    def generate_cid(self) -> int:
+        ...
+
+    def close(self) -> None:
+        ...
+
+
+class PhysicalLayer(Protocol):
+    # pylint: disable=unused-argument,no-self-use
+    def write(self, data: bytes) -> None:
+        ...
+
+    def read(self, size: int, timeout_ms: int) -> bytes:
+        ...
+
+    def close(self) -> None:
+        ...
diff --git a/hwilib/devices/bitbox02_lib/communication/devices.py b/hwilib/devices/bitbox02_lib/communication/devices.py
new file mode 100644
index 000000000..383bf7728
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/devices.py
@@ -0,0 +1,165 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""USB device utility functions"""
+
+import re
+from typing import List
+from typing_extensions import TypedDict
+
+import hid
+import semver
+
+
+BB02MULTI_BOOTLOADER = "bb02-bootloader"
+BB02BTC_BOOTLOADER = "bb02btc-bootloader"
+BITBOX02MULTI = "BitBox02"
+BITBOX02BTC = "BitBox02BTC"
+
+
+class TooManyFoundException(Exception):
+    def __init__(self, count: int) -> None:
+        super().__init__(f"Found {count} devices")
+
+
+class NoneFoundException(Exception):
+    def __init__(self) -> None:
+        super().__init__("Found 0 devices")
+
+
+class DeviceInfo(TypedDict):
+    serial_number: str
+    path: bytes
+    product_string: str
+
+
+def get_devices(product_string: str, interface_number: int = 0) -> List[DeviceInfo]:
+    """
+    Scans devices and returns a list of hid device info objects.
+    """
+    # TODO: product id is 0x2403, but 0x2402 is the id of some dev
+    # device bootloaders. Can be removed in time, not needed for
+    # production devices.
+    # HWW/General endpoint is on interface 0
+    # U2F Endpoint is on interface 1
+    return [
+        info
+        for info in hid.enumerate()
+        if info["vendor_id"] == 0x03EB
+        and info["product_id"] in (0x2402, 0x2403)
+        and (info["usage_page"] == 0xFFFF or info["interface_number"] == interface_number)
+        and info["product_string"] == product_string
+    ]
+
+
+def get_device(product_string: str, interface_number: int = 0) -> DeviceInfo:
+    devices = get_devices(product_string, interface_number)
+    if len(devices) > 1:
+        raise TooManyFoundException(len(devices))
+    if not devices:
+        raise NoneFoundException()
+    return devices[0]
+
+
+def get_bitbox02multi_device() -> DeviceInfo:
+    return get_device(BITBOX02MULTI)
+
+
+def get_bitbox02multi_bootloader() -> DeviceInfo:
+    return get_device(BB02MULTI_BOOTLOADER)
+
+
+def get_bitbox02btc_device() -> DeviceInfo:
+    return get_device(BITBOX02BTC)
+
+
+def get_bitbox02btc_bootloader() -> DeviceInfo:
+    return get_device(BB02BTC_BOOTLOADER)
+
+
+def get_bitbox02multi_devices() -> List[DeviceInfo]:
+    return get_devices(BITBOX02MULTI)
+
+
+def get_bitbox02multi_bootloaders() -> List[DeviceInfo]:
+    return get_devices(BB02MULTI_BOOTLOADER)
+
+
+def get_bitbox02btc_devices() -> List[DeviceInfo]:
+    return get_devices(BITBOX02BTC)
+
+
+def get_bitbox02btc_bootloaders() -> List[DeviceInfo]:
+    return get_devices(BB02BTC_BOOTLOADER)
+
+
+def get_any_bitbox02s() -> List[DeviceInfo]:
+    """
+    Searches for both btc-only and non-btc-only devices
+    Returns:
+        List of devices
+    """
+    devices = get_bitbox02multi_devices()
+    devices.extend(get_bitbox02btc_devices())
+    return devices
+
+
+def get_any_bitbox02() -> DeviceInfo:
+    """
+    Searches for both btc-only and non-btc-only devices
+    Raises:
+        TooManyFoundException: If more than 1 is found
+    Returns:
+        A device if found, otherwise None
+    """
+    devices = get_any_bitbox02s()
+    if len(devices) > 1:
+        raise TooManyFoundException(len(devices))
+    if not devices:
+        raise NoneFoundException()
+    return devices[0]
+
+
+def get_any_bitbox02_bootloaders() -> List[DeviceInfo]:
+    """
+    Searches for both btc-only and non-btc-only devices
+    Returns:
+        List of devices
+    """
+    devices = get_bitbox02multi_bootloaders()
+    devices.extend(get_bitbox02btc_bootloaders())
+    return devices
+
+
+def get_any_bitbox02_bootloader() -> DeviceInfo:
+    """
+    Searches for both btc-only and non-btc-only devices
+    Raises:
+        TooManyFoundException: If more than 1 is found
+    Returns:
+        A device if found, otherwise None
+    """
+    devices = get_any_bitbox02_bootloaders()
+    if len(devices) > 1:
+        raise TooManyFoundException(len(devices))
+    if not devices:
+        raise NoneFoundException
+    return devices[0]
+
+
+def parse_device_version(serial_number: str) -> semver.VersionInfo:
+    match = re.search(r"v([0-9]+\.[0-9]+\.[0-9]+.*)", serial_number)
+    if match is None:
+        raise Exception(f"Could not parse version string from serial_number: {serial_number}")
+
+    return semver.VersionInfo.parse(match.group(1))
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/__init__.py b/hwilib/devices/bitbox02_lib/communication/generated/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/antiklepto_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/antiklepto_pb2.py
new file mode 100644
index 000000000..f4b4c7770
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/antiklepto_pb2.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: antiklepto.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x10\x61ntiklepto.proto\x12\x14shiftcrypto.bitbox02\"3\n\x1d\x41ntiKleptoHostNonceCommitment\x12\x12\n\ncommitment\x18\x01 \x01(\x0c\"0\n\x1a\x41ntiKleptoSignerCommitment\x12\x12\n\ncommitment\x18\x01 \x01(\x0c\"0\n\x1a\x41ntiKleptoSignatureRequest\x12\x12\n\nhost_nonce\x18\x01 \x01(\x0c\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'antiklepto_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _ANTIKLEPTOHOSTNONCECOMMITMENT._serialized_start=42
+  _ANTIKLEPTOHOSTNONCECOMMITMENT._serialized_end=93
+  _ANTIKLEPTOSIGNERCOMMITMENT._serialized_start=95
+  _ANTIKLEPTOSIGNERCOMMITMENT._serialized_end=143
+  _ANTIKLEPTOSIGNATUREREQUEST._serialized_start=145
+  _ANTIKLEPTOSIGNATUREREQUEST._serialized_end=193
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/antiklepto_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/antiklepto_pb2.pyi
new file mode 100644
index 000000000..adc4a83b7
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/antiklepto_pb2.pyi
@@ -0,0 +1,43 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+import builtins
+import google.protobuf.descriptor
+import google.protobuf.message
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class AntiKleptoHostNonceCommitment(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    COMMITMENT_FIELD_NUMBER: builtins.int
+    commitment: builtins.bytes
+    def __init__(self,
+        *,
+        commitment: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["commitment",b"commitment"]) -> None: ...
+global___AntiKleptoHostNonceCommitment = AntiKleptoHostNonceCommitment
+
+class AntiKleptoSignerCommitment(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    COMMITMENT_FIELD_NUMBER: builtins.int
+    commitment: builtins.bytes
+    def __init__(self,
+        *,
+        commitment: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["commitment",b"commitment"]) -> None: ...
+global___AntiKleptoSignerCommitment = AntiKleptoSignerCommitment
+
+class AntiKleptoSignatureRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    HOST_NONCE_FIELD_NUMBER: builtins.int
+    host_nonce: builtins.bytes
+    def __init__(self,
+        *,
+        host_nonce: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["host_nonce",b"host_nonce"]) -> None: ...
+global___AntiKleptoSignatureRequest = AntiKleptoSignatureRequest
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/backup_commands_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/backup_commands_pb2.py
new file mode 100644
index 000000000..3a47ec1b5
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/backup_commands_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: backup_commands.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x62\x61\x63kup_commands.proto\x12\x14shiftcrypto.bitbox02\"$\n\x12\x43heckBackupRequest\x12\x0e\n\x06silent\x18\x01 \x01(\x08\"!\n\x13\x43heckBackupResponse\x12\n\n\x02id\x18\x01 \x01(\t\"A\n\x13\x43reateBackupRequest\x12\x11\n\ttimestamp\x18\x01 \x01(\r\x12\x17\n\x0ftimezone_offset\x18\x02 \x01(\x05\"\x14\n\x12ListBackupsRequest\"9\n\nBackupInfo\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\r\x12\x0c\n\x04name\x18\x04 \x01(\t\"E\n\x13ListBackupsResponse\x12.\n\x04info\x18\x01 \x03(\x0b\x32 .shiftcrypto.bitbox02.BackupInfo\"N\n\x14RestoreBackupRequest\x12\n\n\x02id\x18\x01 \x01(\t\x12\x11\n\ttimestamp\x18\x02 \x01(\r\x12\x17\n\x0ftimezone_offset\x18\x03 \x01(\x05\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'backup_commands_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _CHECKBACKUPREQUEST._serialized_start=47
+  _CHECKBACKUPREQUEST._serialized_end=83
+  _CHECKBACKUPRESPONSE._serialized_start=85
+  _CHECKBACKUPRESPONSE._serialized_end=118
+  _CREATEBACKUPREQUEST._serialized_start=120
+  _CREATEBACKUPREQUEST._serialized_end=185
+  _LISTBACKUPSREQUEST._serialized_start=187
+  _LISTBACKUPSREQUEST._serialized_end=207
+  _BACKUPINFO._serialized_start=209
+  _BACKUPINFO._serialized_end=266
+  _LISTBACKUPSRESPONSE._serialized_start=268
+  _LISTBACKUPSRESPONSE._serialized_end=337
+  _RESTOREBACKUPREQUEST._serialized_start=339
+  _RESTOREBACKUPREQUEST._serialized_end=417
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/backup_commands_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/backup_commands_pb2.pyi
new file mode 100644
index 000000000..77239679d
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/backup_commands_pb2.pyi
@@ -0,0 +1,103 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+import builtins
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.message
+import typing
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class CheckBackupRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    SILENT_FIELD_NUMBER: builtins.int
+    silent: builtins.bool
+    def __init__(self,
+        *,
+        silent: builtins.bool = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["silent",b"silent"]) -> None: ...
+global___CheckBackupRequest = CheckBackupRequest
+
+class CheckBackupResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    ID_FIELD_NUMBER: builtins.int
+    id: typing.Text
+    def __init__(self,
+        *,
+        id: typing.Text = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["id",b"id"]) -> None: ...
+global___CheckBackupResponse = CheckBackupResponse
+
+class CreateBackupRequest(google.protobuf.message.Message):
+    """Timestamp must be in UTC"""
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    TIMESTAMP_FIELD_NUMBER: builtins.int
+    TIMEZONE_OFFSET_FIELD_NUMBER: builtins.int
+    timestamp: builtins.int
+    timezone_offset: builtins.int
+    def __init__(self,
+        *,
+        timestamp: builtins.int = ...,
+        timezone_offset: builtins.int = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["timestamp",b"timestamp","timezone_offset",b"timezone_offset"]) -> None: ...
+global___CreateBackupRequest = CreateBackupRequest
+
+class ListBackupsRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    def __init__(self,
+        ) -> None: ...
+global___ListBackupsRequest = ListBackupsRequest
+
+class BackupInfo(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    ID_FIELD_NUMBER: builtins.int
+    TIMESTAMP_FIELD_NUMBER: builtins.int
+    NAME_FIELD_NUMBER: builtins.int
+    id: typing.Text
+    timestamp: builtins.int
+    name: typing.Text
+    """uint32 timezone_offset = 3;"""
+
+    def __init__(self,
+        *,
+        id: typing.Text = ...,
+        timestamp: builtins.int = ...,
+        name: typing.Text = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["id",b"id","name",b"name","timestamp",b"timestamp"]) -> None: ...
+global___BackupInfo = BackupInfo
+
+class ListBackupsResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    INFO_FIELD_NUMBER: builtins.int
+    @property
+    def info(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BackupInfo]: ...
+    def __init__(self,
+        *,
+        info: typing.Optional[typing.Iterable[global___BackupInfo]] = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["info",b"info"]) -> None: ...
+global___ListBackupsResponse = ListBackupsResponse
+
+class RestoreBackupRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    ID_FIELD_NUMBER: builtins.int
+    TIMESTAMP_FIELD_NUMBER: builtins.int
+    TIMEZONE_OFFSET_FIELD_NUMBER: builtins.int
+    id: typing.Text
+    timestamp: builtins.int
+    timezone_offset: builtins.int
+    def __init__(self,
+        *,
+        id: typing.Text = ...,
+        timestamp: builtins.int = ...,
+        timezone_offset: builtins.int = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["id",b"id","timestamp",b"timestamp","timezone_offset",b"timezone_offset"]) -> None: ...
+global___RestoreBackupRequest = RestoreBackupRequest
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/bitbox02_system_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/bitbox02_system_pb2.py
new file mode 100644
index 000000000..4dbeb63fc
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/bitbox02_system_pb2.py
@@ -0,0 +1,43 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: bitbox02_system.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x15\x62itbox02_system.proto\x12\x14shiftcrypto.bitbox02\"\x14\n\x12\x43heckSDCardRequest\"\'\n\x13\x43heckSDCardResponse\x12\x10\n\x08inserted\x18\x01 \x01(\x08\"\x13\n\x11\x44\x65viceInfoRequest\"\xaf\x01\n\x12\x44\x65viceInfoResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x13\n\x0binitialized\x18\x02 \x01(\x08\x12\x0f\n\x07version\x18\x03 \x01(\t\x12#\n\x1bmnemonic_passphrase_enabled\x18\x04 \x01(\x08\x12&\n\x1emonotonic_increments_remaining\x18\x05 \x01(\r\x12\x18\n\x10securechip_model\x18\x06 \x01(\t\"\x9b\x01\n\x19InsertRemoveSDCardRequest\x12L\n\x06\x61\x63tion\x18\x01 \x01(\x0e\x32<.shiftcrypto.bitbox02.InsertRemoveSDCardRequest.SDCardAction\"0\n\x0cSDCardAction\x12\x0f\n\x0bREMOVE_CARD\x10\x00\x12\x0f\n\x0bINSERT_CARD\x10\x01\"\x0e\n\x0cResetRequest\",\n\x18SetDeviceLanguageRequest\x12\x10\n\x08language\x18\x01 \x01(\t\"$\n\x14SetDeviceNameRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"%\n\x12SetPasswordRequest\x12\x0f\n\x07\x65ntropy\x18\x01 \x01(\x0c\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'bitbox02_system_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _CHECKSDCARDREQUEST._serialized_start=47
+  _CHECKSDCARDREQUEST._serialized_end=67
+  _CHECKSDCARDRESPONSE._serialized_start=69
+  _CHECKSDCARDRESPONSE._serialized_end=108
+  _DEVICEINFOREQUEST._serialized_start=110
+  _DEVICEINFOREQUEST._serialized_end=129
+  _DEVICEINFORESPONSE._serialized_start=132
+  _DEVICEINFORESPONSE._serialized_end=307
+  _INSERTREMOVESDCARDREQUEST._serialized_start=310
+  _INSERTREMOVESDCARDREQUEST._serialized_end=465
+  _INSERTREMOVESDCARDREQUEST_SDCARDACTION._serialized_start=417
+  _INSERTREMOVESDCARDREQUEST_SDCARDACTION._serialized_end=465
+  _RESETREQUEST._serialized_start=467
+  _RESETREQUEST._serialized_end=481
+  _SETDEVICELANGUAGEREQUEST._serialized_start=483
+  _SETDEVICELANGUAGEREQUEST._serialized_end=527
+  _SETDEVICENAMEREQUEST._serialized_start=529
+  _SETDEVICENAMEREQUEST._serialized_end=565
+  _SETPASSWORDREQUEST._serialized_start=567
+  _SETPASSWORDREQUEST._serialized_end=604
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/bitbox02_system_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/bitbox02_system_pb2.pyi
new file mode 100644
index 000000000..6b6829e2c
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/bitbox02_system_pb2.pyi
@@ -0,0 +1,126 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+import builtins
+import google.protobuf.descriptor
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import typing
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class CheckSDCardRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    def __init__(self,
+        ) -> None: ...
+global___CheckSDCardRequest = CheckSDCardRequest
+
+class CheckSDCardResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    INSERTED_FIELD_NUMBER: builtins.int
+    inserted: builtins.bool
+    def __init__(self,
+        *,
+        inserted: builtins.bool = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["inserted",b"inserted"]) -> None: ...
+global___CheckSDCardResponse = CheckSDCardResponse
+
+class DeviceInfoRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    def __init__(self,
+        ) -> None: ...
+global___DeviceInfoRequest = DeviceInfoRequest
+
+class DeviceInfoResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    NAME_FIELD_NUMBER: builtins.int
+    INITIALIZED_FIELD_NUMBER: builtins.int
+    VERSION_FIELD_NUMBER: builtins.int
+    MNEMONIC_PASSPHRASE_ENABLED_FIELD_NUMBER: builtins.int
+    MONOTONIC_INCREMENTS_REMAINING_FIELD_NUMBER: builtins.int
+    SECURECHIP_MODEL_FIELD_NUMBER: builtins.int
+    name: typing.Text
+    initialized: builtins.bool
+    version: typing.Text
+    mnemonic_passphrase_enabled: builtins.bool
+    monotonic_increments_remaining: builtins.int
+    securechip_model: typing.Text
+    """From v9.6.0: "ATECC608A" or "ATECC608B"."""
+
+    def __init__(self,
+        *,
+        name: typing.Text = ...,
+        initialized: builtins.bool = ...,
+        version: typing.Text = ...,
+        mnemonic_passphrase_enabled: builtins.bool = ...,
+        monotonic_increments_remaining: builtins.int = ...,
+        securechip_model: typing.Text = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["initialized",b"initialized","mnemonic_passphrase_enabled",b"mnemonic_passphrase_enabled","monotonic_increments_remaining",b"monotonic_increments_remaining","name",b"name","securechip_model",b"securechip_model","version",b"version"]) -> None: ...
+global___DeviceInfoResponse = DeviceInfoResponse
+
+class InsertRemoveSDCardRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _SDCardAction:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _SDCardActionEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[InsertRemoveSDCardRequest._SDCardAction.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        REMOVE_CARD: InsertRemoveSDCardRequest._SDCardAction.ValueType  # 0
+        INSERT_CARD: InsertRemoveSDCardRequest._SDCardAction.ValueType  # 1
+    class SDCardAction(_SDCardAction, metaclass=_SDCardActionEnumTypeWrapper):
+        pass
+
+    REMOVE_CARD: InsertRemoveSDCardRequest.SDCardAction.ValueType  # 0
+    INSERT_CARD: InsertRemoveSDCardRequest.SDCardAction.ValueType  # 1
+
+    ACTION_FIELD_NUMBER: builtins.int
+    action: global___InsertRemoveSDCardRequest.SDCardAction.ValueType
+    def __init__(self,
+        *,
+        action: global___InsertRemoveSDCardRequest.SDCardAction.ValueType = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["action",b"action"]) -> None: ...
+global___InsertRemoveSDCardRequest = InsertRemoveSDCardRequest
+
+class ResetRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    def __init__(self,
+        ) -> None: ...
+global___ResetRequest = ResetRequest
+
+class SetDeviceLanguageRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    LANGUAGE_FIELD_NUMBER: builtins.int
+    language: typing.Text
+    def __init__(self,
+        *,
+        language: typing.Text = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["language",b"language"]) -> None: ...
+global___SetDeviceLanguageRequest = SetDeviceLanguageRequest
+
+class SetDeviceNameRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    NAME_FIELD_NUMBER: builtins.int
+    name: typing.Text
+    def __init__(self,
+        *,
+        name: typing.Text = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["name",b"name"]) -> None: ...
+global___SetDeviceNameRequest = SetDeviceNameRequest
+
+class SetPasswordRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    ENTROPY_FIELD_NUMBER: builtins.int
+    entropy: builtins.bytes
+    def __init__(self,
+        *,
+        entropy: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["entropy",b"entropy"]) -> None: ...
+global___SetPasswordRequest = SetPasswordRequest
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/btc_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/btc_pb2.py
new file mode 100644
index 000000000..73a1e179e
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/btc_pb2.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: btc.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from . import common_pb2 as common__pb2
+from . import antiklepto_pb2 as antiklepto__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\tbtc.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xb9\x03\n\x0f\x42TCScriptConfig\x12G\n\x0bsimple_type\x18\x01 \x01(\x0e\x32\x30.shiftcrypto.bitbox02.BTCScriptConfig.SimpleTypeH\x00\x12\x42\n\x08multisig\x18\x02 \x01(\x0b\x32..shiftcrypto.bitbox02.BTCScriptConfig.MultisigH\x00\x1a\xd9\x01\n\x08Multisig\x12\x11\n\tthreshold\x18\x01 \x01(\r\x12)\n\x05xpubs\x18\x02 \x03(\x0b\x32\x1a.shiftcrypto.bitbox02.XPub\x12\x16\n\x0eour_xpub_index\x18\x03 \x01(\r\x12N\n\x0bscript_type\x18\x04 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.BTCScriptConfig.Multisig.ScriptType\"\'\n\nScriptType\x12\t\n\x05P2WSH\x10\x00\x12\x0e\n\nP2WSH_P2SH\x10\x01\"3\n\nSimpleType\x12\x0f\n\x0bP2WPKH_P2SH\x10\x00\x12\n\n\x06P2WPKH\x10\x01\x12\x08\n\x04P2TR\x10\x02\x42\x08\n\x06\x63onfig\"\xfc\x02\n\rBTCPubRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x41\n\txpub_type\x18\x03 \x01(\x0e\x32,.shiftcrypto.bitbox02.BTCPubRequest.XPubTypeH\x00\x12>\n\rscript_config\x18\x04 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfigH\x00\x12\x0f\n\x07\x64isplay\x18\x05 \x01(\x08\"\x8e\x01\n\x08XPubType\x12\x08\n\x04TPUB\x10\x00\x12\x08\n\x04XPUB\x10\x01\x12\x08\n\x04YPUB\x10\x02\x12\x08\n\x04ZPUB\x10\x03\x12\x08\n\x04VPUB\x10\x04\x12\x08\n\x04UPUB\x10\x05\x12\x10\n\x0c\x43\x41PITAL_VPUB\x10\x06\x12\x10\n\x0c\x43\x41PITAL_ZPUB\x10\x07\x12\x10\n\x0c\x43\x41PITAL_UPUB\x10\x08\x12\x10\n\x0c\x43\x41PITAL_YPUB\x10\tB\x08\n\x06output\"k\n\x1a\x42TCScriptConfigWithKeypath\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\xc5\x02\n\x12\x42TCSignInitRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12H\n\x0escript_configs\x18\x02 \x03(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0f\n\x07version\x18\x04 \x01(\r\x12\x12\n\nnum_inputs\x18\x05 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x06 \x01(\r\x12\x10\n\x08locktime\x18\x07 \x01(\r\x12H\n\x0b\x66ormat_unit\x18\x08 \x01(\x0e\x32\x33.shiftcrypto.bitbox02.BTCSignInitRequest.FormatUnit\"\"\n\nFormatUnit\x12\x0b\n\x07\x44\x45\x46\x41ULT\x10\x00\x12\x07\n\x03SAT\x10\x01\"\xe8\x02\n\x13\x42TCSignNextResponse\x12<\n\x04type\x18\x01 \x01(\x0e\x32..shiftcrypto.bitbox02.BTCSignNextResponse.Type\x12\r\n\x05index\x18\x02 \x01(\r\x12\x15\n\rhas_signature\x18\x03 \x01(\x08\x12\x11\n\tsignature\x18\x04 \x01(\x0c\x12\x12\n\nprev_index\x18\x05 \x01(\r\x12W\n\x1d\x61nti_klepto_signer_commitment\x18\x06 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitment\"m\n\x04Type\x12\t\n\x05INPUT\x10\x00\x12\n\n\x06OUTPUT\x10\x01\x12\x08\n\x04\x44ONE\x10\x02\x12\x0f\n\x0bPREVTX_INIT\x10\x03\x12\x10\n\x0cPREVTX_INPUT\x10\x04\x12\x11\n\rPREVTX_OUTPUT\x10\x05\x12\x0e\n\nHOST_NONCE\x10\x06\"\xea\x01\n\x13\x42TCSignInputRequest\x12\x13\n\x0bprevOutHash\x18\x01 \x01(\x0c\x12\x14\n\x0cprevOutIndex\x18\x02 \x01(\r\x12\x14\n\x0cprevOutValue\x18\x03 \x01(\x04\x12\x10\n\x08sequence\x18\x04 \x01(\r\x12\x0f\n\x07keypath\x18\x06 \x03(\r\x12\x1b\n\x13script_config_index\x18\x07 \x01(\r\x12R\n\x15host_nonce_commitment\x18\x08 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"\xa5\x01\n\x14\x42TCSignOutputRequest\x12\x0c\n\x04ours\x18\x01 \x01(\x08\x12\x31\n\x04type\x18\x02 \x01(\x0e\x32#.shiftcrypto.bitbox02.BTCOutputType\x12\r\n\x05value\x18\x03 \x01(\x04\x12\x0f\n\x07payload\x18\x04 \x01(\x0c\x12\x0f\n\x07keypath\x18\x05 \x03(\r\x12\x1b\n\x13script_config_index\x18\x06 \x01(\r\"\x99\x01\n\x1b\x42TCScriptConfigRegistration\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12<\n\rscript_config\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.BTCScriptConfig\x12\x0f\n\x07keypath\x18\x03 \x03(\r\"\x0c\n\nBTCSuccess\"m\n\"BTCIsScriptConfigRegisteredRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\"<\n#BTCIsScriptConfigRegisteredResponse\x12\x15\n\ris_registered\x18\x01 \x01(\x08\"\xfc\x01\n\x1e\x42TCRegisterScriptConfigRequest\x12G\n\x0cregistration\x18\x01 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.BTCScriptConfigRegistration\x12\x0c\n\x04name\x18\x02 \x01(\t\x12P\n\txpub_type\x18\x03 \x01(\x0e\x32=.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequest.XPubType\"1\n\x08XPubType\x12\x11\n\rAUTO_ELECTRUM\x10\x00\x12\x12\n\x0e\x41UTO_XPUB_TPUB\x10\x01\"b\n\x14\x42TCPrevTxInitRequest\x12\x0f\n\x07version\x18\x01 \x01(\r\x12\x12\n\nnum_inputs\x18\x02 \x01(\r\x12\x13\n\x0bnum_outputs\x18\x03 \x01(\r\x12\x10\n\x08locktime\x18\x04 \x01(\r\"r\n\x15\x42TCPrevTxInputRequest\x12\x15\n\rprev_out_hash\x18\x01 \x01(\x0c\x12\x16\n\x0eprev_out_index\x18\x02 \x01(\r\x12\x18\n\x10signature_script\x18\x03 \x01(\x0c\x12\x10\n\x08sequence\x18\x04 \x01(\r\">\n\x16\x42TCPrevTxOutputRequest\x12\r\n\x05value\x18\x01 \x01(\x04\x12\x15\n\rpubkey_script\x18\x02 \x01(\x0c\"\xee\x01\n\x15\x42TCSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.BTCCoin\x12G\n\rscript_config\x18\x02 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.BTCScriptConfigWithKeypath\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\"+\n\x16\x42TCSignMessageResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\xb6\x04\n\nBTCRequest\x12_\n\x1bis_script_config_registered\x18\x01 \x01(\x0b\x32\x38.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredRequestH\x00\x12V\n\x16register_script_config\x18\x02 \x01(\x0b\x32\x34.shiftcrypto.bitbox02.BTCRegisterScriptConfigRequestH\x00\x12\x41\n\x0bprevtx_init\x18\x03 \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCPrevTxInitRequestH\x00\x12\x43\n\x0cprevtx_input\x18\x04 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCPrevTxInputRequestH\x00\x12\x45\n\rprevtx_output\x18\x05 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCPrevTxOutputRequestH\x00\x12\x43\n\x0csign_message\x18\x06 \x01(\x0b\x32+.shiftcrypto.bitbox02.BTCSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x07 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x42\t\n\x07request\"\x90\x03\n\x0b\x42TCResponse\x12\x33\n\x07success\x18\x01 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCSuccessH\x00\x12`\n\x1bis_script_config_registered\x18\x02 \x01(\x0b\x32\x39.shiftcrypto.bitbox02.BTCIsScriptConfigRegisteredResponseH\x00\x12>\n\tsign_next\x18\x03 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x44\n\x0csign_message\x18\x04 \x01(\x0b\x32,.shiftcrypto.bitbox02.BTCSignMessageResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x42\n\n\x08response*/\n\x07\x42TCCoin\x12\x07\n\x03\x42TC\x10\x00\x12\x08\n\x04TBTC\x10\x01\x12\x07\n\x03LTC\x10\x02\x12\x08\n\x04TLTC\x10\x03*R\n\rBTCOutputType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05P2PKH\x10\x01\x12\x08\n\x04P2SH\x10\x02\x12\n\n\x06P2WPKH\x10\x03\x12\t\n\x05P2WSH\x10\x04\x12\x08\n\x04P2TR\x10\x05\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'btc_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _BTCCOIN._serialized_start=4235
+  _BTCCOIN._serialized_end=4282
+  _BTCOUTPUTTYPE._serialized_start=4284
+  _BTCOUTPUTTYPE._serialized_end=4366
+  _BTCSCRIPTCONFIG._serialized_start=68
+  _BTCSCRIPTCONFIG._serialized_end=509
+  _BTCSCRIPTCONFIG_MULTISIG._serialized_start=229
+  _BTCSCRIPTCONFIG_MULTISIG._serialized_end=446
+  _BTCSCRIPTCONFIG_MULTISIG_SCRIPTTYPE._serialized_start=407
+  _BTCSCRIPTCONFIG_MULTISIG_SCRIPTTYPE._serialized_end=446
+  _BTCSCRIPTCONFIG_SIMPLETYPE._serialized_start=448
+  _BTCSCRIPTCONFIG_SIMPLETYPE._serialized_end=499
+  _BTCPUBREQUEST._serialized_start=512
+  _BTCPUBREQUEST._serialized_end=892
+  _BTCPUBREQUEST_XPUBTYPE._serialized_start=740
+  _BTCPUBREQUEST_XPUBTYPE._serialized_end=882
+  _BTCSCRIPTCONFIGWITHKEYPATH._serialized_start=894
+  _BTCSCRIPTCONFIGWITHKEYPATH._serialized_end=1001
+  _BTCSIGNINITREQUEST._serialized_start=1004
+  _BTCSIGNINITREQUEST._serialized_end=1329
+  _BTCSIGNINITREQUEST_FORMATUNIT._serialized_start=1295
+  _BTCSIGNINITREQUEST_FORMATUNIT._serialized_end=1329
+  _BTCSIGNNEXTRESPONSE._serialized_start=1332
+  _BTCSIGNNEXTRESPONSE._serialized_end=1692
+  _BTCSIGNNEXTRESPONSE_TYPE._serialized_start=1583
+  _BTCSIGNNEXTRESPONSE_TYPE._serialized_end=1692
+  _BTCSIGNINPUTREQUEST._serialized_start=1695
+  _BTCSIGNINPUTREQUEST._serialized_end=1929
+  _BTCSIGNOUTPUTREQUEST._serialized_start=1932
+  _BTCSIGNOUTPUTREQUEST._serialized_end=2097
+  _BTCSCRIPTCONFIGREGISTRATION._serialized_start=2100
+  _BTCSCRIPTCONFIGREGISTRATION._serialized_end=2253
+  _BTCSUCCESS._serialized_start=2255
+  _BTCSUCCESS._serialized_end=2267
+  _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_start=2269
+  _BTCISSCRIPTCONFIGREGISTEREDREQUEST._serialized_end=2378
+  _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_start=2380
+  _BTCISSCRIPTCONFIGREGISTEREDRESPONSE._serialized_end=2440
+  _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_start=2443
+  _BTCREGISTERSCRIPTCONFIGREQUEST._serialized_end=2695
+  _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_start=2646
+  _BTCREGISTERSCRIPTCONFIGREQUEST_XPUBTYPE._serialized_end=2695
+  _BTCPREVTXINITREQUEST._serialized_start=2697
+  _BTCPREVTXINITREQUEST._serialized_end=2795
+  _BTCPREVTXINPUTREQUEST._serialized_start=2797
+  _BTCPREVTXINPUTREQUEST._serialized_end=2911
+  _BTCPREVTXOUTPUTREQUEST._serialized_start=2913
+  _BTCPREVTXOUTPUTREQUEST._serialized_end=2975
+  _BTCSIGNMESSAGEREQUEST._serialized_start=2978
+  _BTCSIGNMESSAGEREQUEST._serialized_end=3216
+  _BTCSIGNMESSAGERESPONSE._serialized_start=3218
+  _BTCSIGNMESSAGERESPONSE._serialized_end=3261
+  _BTCREQUEST._serialized_start=3264
+  _BTCREQUEST._serialized_end=3830
+  _BTCRESPONSE._serialized_start=3833
+  _BTCRESPONSE._serialized_end=4233
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/btc_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/btc_pb2.pyi
new file mode 100644
index 000000000..a4e69cff2
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/btc_pb2.pyi
@@ -0,0 +1,681 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+from . import antiklepto_pb2
+import builtins
+from . import common_pb2
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import typing
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class _BTCCoin:
+    ValueType = typing.NewType('ValueType', builtins.int)
+    V: typing_extensions.TypeAlias = ValueType
+class _BTCCoinEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_BTCCoin.ValueType], builtins.type):
+    DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+    BTC: _BTCCoin.ValueType  # 0
+    TBTC: _BTCCoin.ValueType  # 1
+    LTC: _BTCCoin.ValueType  # 2
+    TLTC: _BTCCoin.ValueType  # 3
+class BTCCoin(_BTCCoin, metaclass=_BTCCoinEnumTypeWrapper):
+    pass
+
+BTC: BTCCoin.ValueType  # 0
+TBTC: BTCCoin.ValueType  # 1
+LTC: BTCCoin.ValueType  # 2
+TLTC: BTCCoin.ValueType  # 3
+global___BTCCoin = BTCCoin
+
+
+class _BTCOutputType:
+    ValueType = typing.NewType('ValueType', builtins.int)
+    V: typing_extensions.TypeAlias = ValueType
+class _BTCOutputTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_BTCOutputType.ValueType], builtins.type):
+    DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+    UNKNOWN: _BTCOutputType.ValueType  # 0
+    P2PKH: _BTCOutputType.ValueType  # 1
+    P2SH: _BTCOutputType.ValueType  # 2
+    P2WPKH: _BTCOutputType.ValueType  # 3
+    P2WSH: _BTCOutputType.ValueType  # 4
+    P2TR: _BTCOutputType.ValueType  # 5
+class BTCOutputType(_BTCOutputType, metaclass=_BTCOutputTypeEnumTypeWrapper):
+    pass
+
+UNKNOWN: BTCOutputType.ValueType  # 0
+P2PKH: BTCOutputType.ValueType  # 1
+P2SH: BTCOutputType.ValueType  # 2
+P2WPKH: BTCOutputType.ValueType  # 3
+P2WSH: BTCOutputType.ValueType  # 4
+P2TR: BTCOutputType.ValueType  # 5
+global___BTCOutputType = BTCOutputType
+
+
+class BTCScriptConfig(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _SimpleType:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _SimpleTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BTCScriptConfig._SimpleType.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        P2WPKH_P2SH: BTCScriptConfig._SimpleType.ValueType  # 0
+        P2WPKH: BTCScriptConfig._SimpleType.ValueType  # 1
+        P2TR: BTCScriptConfig._SimpleType.ValueType  # 2
+    class SimpleType(_SimpleType, metaclass=_SimpleTypeEnumTypeWrapper):
+        """SimpleType is a "simple" script: one public key, no additional inputs."""
+        pass
+
+    P2WPKH_P2SH: BTCScriptConfig.SimpleType.ValueType  # 0
+    P2WPKH: BTCScriptConfig.SimpleType.ValueType  # 1
+    P2TR: BTCScriptConfig.SimpleType.ValueType  # 2
+
+    class Multisig(google.protobuf.message.Message):
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        class _ScriptType:
+            ValueType = typing.NewType('ValueType', builtins.int)
+            V: typing_extensions.TypeAlias = ValueType
+        class _ScriptTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BTCScriptConfig.Multisig._ScriptType.ValueType], builtins.type):
+            DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+            P2WSH: BTCScriptConfig.Multisig._ScriptType.ValueType  # 0
+            """native segwit v0 multisig (bech32 addresses)"""
+
+            P2WSH_P2SH: BTCScriptConfig.Multisig._ScriptType.ValueType  # 1
+            """wrapped segwit for legacy address compatibility"""
+
+        class ScriptType(_ScriptType, metaclass=_ScriptTypeEnumTypeWrapper):
+            pass
+
+        P2WSH: BTCScriptConfig.Multisig.ScriptType.ValueType  # 0
+        """native segwit v0 multisig (bech32 addresses)"""
+
+        P2WSH_P2SH: BTCScriptConfig.Multisig.ScriptType.ValueType  # 1
+        """wrapped segwit for legacy address compatibility"""
+
+
+        THRESHOLD_FIELD_NUMBER: builtins.int
+        XPUBS_FIELD_NUMBER: builtins.int
+        OUR_XPUB_INDEX_FIELD_NUMBER: builtins.int
+        SCRIPT_TYPE_FIELD_NUMBER: builtins.int
+        threshold: builtins.int
+        @property
+        def xpubs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[common_pb2.XPub]:
+            """xpubs are acount-level xpubs. Addresses are going to be derived from it using: m/<change>/<receive>.
+            The number of xpubs defines the number of cosigners.
+            """
+            pass
+        our_xpub_index: builtins.int
+        """Index to the xpub of our keystore in xpubs. The keypath to it is provided via
+        BTCPubRequest/BTCSignInit.
+        """
+
+        script_type: global___BTCScriptConfig.Multisig.ScriptType.ValueType
+        def __init__(self,
+            *,
+            threshold: builtins.int = ...,
+            xpubs: typing.Optional[typing.Iterable[common_pb2.XPub]] = ...,
+            our_xpub_index: builtins.int = ...,
+            script_type: global___BTCScriptConfig.Multisig.ScriptType.ValueType = ...,
+            ) -> None: ...
+        def ClearField(self, field_name: typing_extensions.Literal["our_xpub_index",b"our_xpub_index","script_type",b"script_type","threshold",b"threshold","xpubs",b"xpubs"]) -> None: ...
+
+    SIMPLE_TYPE_FIELD_NUMBER: builtins.int
+    MULTISIG_FIELD_NUMBER: builtins.int
+    simple_type: global___BTCScriptConfig.SimpleType.ValueType
+    @property
+    def multisig(self) -> global___BTCScriptConfig.Multisig: ...
+    def __init__(self,
+        *,
+        simple_type: global___BTCScriptConfig.SimpleType.ValueType = ...,
+        multisig: typing.Optional[global___BTCScriptConfig.Multisig] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["config",b"config","multisig",b"multisig","simple_type",b"simple_type"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["config",b"config","multisig",b"multisig","simple_type",b"simple_type"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["config",b"config"]) -> typing.Optional[typing_extensions.Literal["simple_type","multisig"]]: ...
+global___BTCScriptConfig = BTCScriptConfig
+
+class BTCPubRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _XPubType:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _XPubTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BTCPubRequest._XPubType.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        TPUB: BTCPubRequest._XPubType.ValueType  # 0
+        XPUB: BTCPubRequest._XPubType.ValueType  # 1
+        YPUB: BTCPubRequest._XPubType.ValueType  # 2
+        ZPUB: BTCPubRequest._XPubType.ValueType  # 3
+        """zpub"""
+
+        VPUB: BTCPubRequest._XPubType.ValueType  # 4
+        """vpub"""
+
+        UPUB: BTCPubRequest._XPubType.ValueType  # 5
+        CAPITAL_VPUB: BTCPubRequest._XPubType.ValueType  # 6
+        """Vpub"""
+
+        CAPITAL_ZPUB: BTCPubRequest._XPubType.ValueType  # 7
+        """Zpub"""
+
+        CAPITAL_UPUB: BTCPubRequest._XPubType.ValueType  # 8
+        """Upub"""
+
+        CAPITAL_YPUB: BTCPubRequest._XPubType.ValueType  # 9
+        """Ypub"""
+
+    class XPubType(_XPubType, metaclass=_XPubTypeEnumTypeWrapper):
+        pass
+
+    TPUB: BTCPubRequest.XPubType.ValueType  # 0
+    XPUB: BTCPubRequest.XPubType.ValueType  # 1
+    YPUB: BTCPubRequest.XPubType.ValueType  # 2
+    ZPUB: BTCPubRequest.XPubType.ValueType  # 3
+    """zpub"""
+
+    VPUB: BTCPubRequest.XPubType.ValueType  # 4
+    """vpub"""
+
+    UPUB: BTCPubRequest.XPubType.ValueType  # 5
+    CAPITAL_VPUB: BTCPubRequest.XPubType.ValueType  # 6
+    """Vpub"""
+
+    CAPITAL_ZPUB: BTCPubRequest.XPubType.ValueType  # 7
+    """Zpub"""
+
+    CAPITAL_UPUB: BTCPubRequest.XPubType.ValueType  # 8
+    """Upub"""
+
+    CAPITAL_YPUB: BTCPubRequest.XPubType.ValueType  # 9
+    """Ypub"""
+
+
+    COIN_FIELD_NUMBER: builtins.int
+    KEYPATH_FIELD_NUMBER: builtins.int
+    XPUB_TYPE_FIELD_NUMBER: builtins.int
+    SCRIPT_CONFIG_FIELD_NUMBER: builtins.int
+    DISPLAY_FIELD_NUMBER: builtins.int
+    coin: global___BTCCoin.ValueType
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    xpub_type: global___BTCPubRequest.XPubType.ValueType
+    @property
+    def script_config(self) -> global___BTCScriptConfig: ...
+    display: builtins.bool
+    def __init__(self,
+        *,
+        coin: global___BTCCoin.ValueType = ...,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        xpub_type: global___BTCPubRequest.XPubType.ValueType = ...,
+        script_config: typing.Optional[global___BTCScriptConfig] = ...,
+        display: builtins.bool = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["output",b"output","script_config",b"script_config","xpub_type",b"xpub_type"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["coin",b"coin","display",b"display","keypath",b"keypath","output",b"output","script_config",b"script_config","xpub_type",b"xpub_type"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["output",b"output"]) -> typing.Optional[typing_extensions.Literal["xpub_type","script_config"]]: ...
+global___BTCPubRequest = BTCPubRequest
+
+class BTCScriptConfigWithKeypath(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    SCRIPT_CONFIG_FIELD_NUMBER: builtins.int
+    KEYPATH_FIELD_NUMBER: builtins.int
+    @property
+    def script_config(self) -> global___BTCScriptConfig: ...
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    def __init__(self,
+        *,
+        script_config: typing.Optional[global___BTCScriptConfig] = ...,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["script_config",b"script_config"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["keypath",b"keypath","script_config",b"script_config"]) -> None: ...
+global___BTCScriptConfigWithKeypath = BTCScriptConfigWithKeypath
+
+class BTCSignInitRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _FormatUnit:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _FormatUnitEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BTCSignInitRequest._FormatUnit.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        DEFAULT: BTCSignInitRequest._FormatUnit.ValueType  # 0
+        """According to `coin` (BTC, LTC, etc.)."""
+
+        SAT: BTCSignInitRequest._FormatUnit.ValueType  # 1
+        """Only valid for BTC/TBTC, formats as "sat"/"tsat"."""
+
+    class FormatUnit(_FormatUnit, metaclass=_FormatUnitEnumTypeWrapper):
+        pass
+
+    DEFAULT: BTCSignInitRequest.FormatUnit.ValueType  # 0
+    """According to `coin` (BTC, LTC, etc.)."""
+
+    SAT: BTCSignInitRequest.FormatUnit.ValueType  # 1
+    """Only valid for BTC/TBTC, formats as "sat"/"tsat"."""
+
+
+    COIN_FIELD_NUMBER: builtins.int
+    SCRIPT_CONFIGS_FIELD_NUMBER: builtins.int
+    VERSION_FIELD_NUMBER: builtins.int
+    NUM_INPUTS_FIELD_NUMBER: builtins.int
+    NUM_OUTPUTS_FIELD_NUMBER: builtins.int
+    LOCKTIME_FIELD_NUMBER: builtins.int
+    FORMAT_UNIT_FIELD_NUMBER: builtins.int
+    coin: global___BTCCoin.ValueType
+    @property
+    def script_configs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___BTCScriptConfigWithKeypath]:
+        """used script configs in inputs and changes"""
+        pass
+    version: builtins.int
+    """must be 1 or 2"""
+
+    num_inputs: builtins.int
+    num_outputs: builtins.int
+    locktime: builtins.int
+    """must be <500000000"""
+
+    format_unit: global___BTCSignInitRequest.FormatUnit.ValueType
+    def __init__(self,
+        *,
+        coin: global___BTCCoin.ValueType = ...,
+        script_configs: typing.Optional[typing.Iterable[global___BTCScriptConfigWithKeypath]] = ...,
+        version: builtins.int = ...,
+        num_inputs: builtins.int = ...,
+        num_outputs: builtins.int = ...,
+        locktime: builtins.int = ...,
+        format_unit: global___BTCSignInitRequest.FormatUnit.ValueType = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["coin",b"coin","format_unit",b"format_unit","locktime",b"locktime","num_inputs",b"num_inputs","num_outputs",b"num_outputs","script_configs",b"script_configs","version",b"version"]) -> None: ...
+global___BTCSignInitRequest = BTCSignInitRequest
+
+class BTCSignNextResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _Type:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _TypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BTCSignNextResponse._Type.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        INPUT: BTCSignNextResponse._Type.ValueType  # 0
+        OUTPUT: BTCSignNextResponse._Type.ValueType  # 1
+        DONE: BTCSignNextResponse._Type.ValueType  # 2
+        PREVTX_INIT: BTCSignNextResponse._Type.ValueType  # 3
+        """For the previous transaction at input `index`."""
+
+        PREVTX_INPUT: BTCSignNextResponse._Type.ValueType  # 4
+        PREVTX_OUTPUT: BTCSignNextResponse._Type.ValueType  # 5
+        HOST_NONCE: BTCSignNextResponse._Type.ValueType  # 6
+    class Type(_Type, metaclass=_TypeEnumTypeWrapper):
+        pass
+
+    INPUT: BTCSignNextResponse.Type.ValueType  # 0
+    OUTPUT: BTCSignNextResponse.Type.ValueType  # 1
+    DONE: BTCSignNextResponse.Type.ValueType  # 2
+    PREVTX_INIT: BTCSignNextResponse.Type.ValueType  # 3
+    """For the previous transaction at input `index`."""
+
+    PREVTX_INPUT: BTCSignNextResponse.Type.ValueType  # 4
+    PREVTX_OUTPUT: BTCSignNextResponse.Type.ValueType  # 5
+    HOST_NONCE: BTCSignNextResponse.Type.ValueType  # 6
+
+    TYPE_FIELD_NUMBER: builtins.int
+    INDEX_FIELD_NUMBER: builtins.int
+    HAS_SIGNATURE_FIELD_NUMBER: builtins.int
+    SIGNATURE_FIELD_NUMBER: builtins.int
+    PREV_INDEX_FIELD_NUMBER: builtins.int
+    ANTI_KLEPTO_SIGNER_COMMITMENT_FIELD_NUMBER: builtins.int
+    type: global___BTCSignNextResponse.Type.ValueType
+    index: builtins.int
+    """index of the current input or output"""
+
+    has_signature: builtins.bool
+    """only as a response to BTCSignInputRequest"""
+
+    signature: builtins.bytes
+    """64 bytes (32 bytes big endian R, 32 bytes big endian S). Only if has_signature is true."""
+
+    prev_index: builtins.int
+    """Previous tx's input/output index in case of PREV_INPUT or PREV_OUTPUT, for the input at `index`."""
+
+    @property
+    def anti_klepto_signer_commitment(self) -> antiklepto_pb2.AntiKleptoSignerCommitment: ...
+    def __init__(self,
+        *,
+        type: global___BTCSignNextResponse.Type.ValueType = ...,
+        index: builtins.int = ...,
+        has_signature: builtins.bool = ...,
+        signature: builtins.bytes = ...,
+        prev_index: builtins.int = ...,
+        anti_klepto_signer_commitment: typing.Optional[antiklepto_pb2.AntiKleptoSignerCommitment] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["anti_klepto_signer_commitment",b"anti_klepto_signer_commitment"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["anti_klepto_signer_commitment",b"anti_klepto_signer_commitment","has_signature",b"has_signature","index",b"index","prev_index",b"prev_index","signature",b"signature","type",b"type"]) -> None: ...
+global___BTCSignNextResponse = BTCSignNextResponse
+
+class BTCSignInputRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    PREVOUTHASH_FIELD_NUMBER: builtins.int
+    PREVOUTINDEX_FIELD_NUMBER: builtins.int
+    PREVOUTVALUE_FIELD_NUMBER: builtins.int
+    SEQUENCE_FIELD_NUMBER: builtins.int
+    KEYPATH_FIELD_NUMBER: builtins.int
+    SCRIPT_CONFIG_INDEX_FIELD_NUMBER: builtins.int
+    HOST_NONCE_COMMITMENT_FIELD_NUMBER: builtins.int
+    prevOutHash: builtins.bytes
+    prevOutIndex: builtins.int
+    prevOutValue: builtins.int
+    sequence: builtins.int
+    """must be 0xffffffff-2, 0xffffffff-1 or 0xffffffff"""
+
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]:
+        """all inputs must be ours."""
+        pass
+    script_config_index: builtins.int
+    """References a script config from BTCSignInitRequest"""
+
+    @property
+    def host_nonce_commitment(self) -> antiklepto_pb2.AntiKleptoHostNonceCommitment: ...
+    def __init__(self,
+        *,
+        prevOutHash: builtins.bytes = ...,
+        prevOutIndex: builtins.int = ...,
+        prevOutValue: builtins.int = ...,
+        sequence: builtins.int = ...,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        script_config_index: builtins.int = ...,
+        host_nonce_commitment: typing.Optional[antiklepto_pb2.AntiKleptoHostNonceCommitment] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["host_nonce_commitment",b"host_nonce_commitment"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["host_nonce_commitment",b"host_nonce_commitment","keypath",b"keypath","prevOutHash",b"prevOutHash","prevOutIndex",b"prevOutIndex","prevOutValue",b"prevOutValue","script_config_index",b"script_config_index","sequence",b"sequence"]) -> None: ...
+global___BTCSignInputRequest = BTCSignInputRequest
+
+class BTCSignOutputRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    OURS_FIELD_NUMBER: builtins.int
+    TYPE_FIELD_NUMBER: builtins.int
+    VALUE_FIELD_NUMBER: builtins.int
+    PAYLOAD_FIELD_NUMBER: builtins.int
+    KEYPATH_FIELD_NUMBER: builtins.int
+    SCRIPT_CONFIG_INDEX_FIELD_NUMBER: builtins.int
+    ours: builtins.bool
+    type: global___BTCOutputType.ValueType
+    """if ours is false"""
+
+    value: builtins.int
+    """20 bytes for p2pkh, p2sh, pw2wpkh. 32 bytes for p2wsh."""
+
+    payload: builtins.bytes
+    """if ours is false. Renamed from `hash`."""
+
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]:
+        """if ours is true"""
+        pass
+    script_config_index: builtins.int
+    """If ours is true. References a script config from BTCSignInitRequest"""
+
+    def __init__(self,
+        *,
+        ours: builtins.bool = ...,
+        type: global___BTCOutputType.ValueType = ...,
+        value: builtins.int = ...,
+        payload: builtins.bytes = ...,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        script_config_index: builtins.int = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["keypath",b"keypath","ours",b"ours","payload",b"payload","script_config_index",b"script_config_index","type",b"type","value",b"value"]) -> None: ...
+global___BTCSignOutputRequest = BTCSignOutputRequest
+
+class BTCScriptConfigRegistration(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    COIN_FIELD_NUMBER: builtins.int
+    SCRIPT_CONFIG_FIELD_NUMBER: builtins.int
+    KEYPATH_FIELD_NUMBER: builtins.int
+    coin: global___BTCCoin.ValueType
+    @property
+    def script_config(self) -> global___BTCScriptConfig: ...
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    def __init__(self,
+        *,
+        coin: global___BTCCoin.ValueType = ...,
+        script_config: typing.Optional[global___BTCScriptConfig] = ...,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["script_config",b"script_config"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["coin",b"coin","keypath",b"keypath","script_config",b"script_config"]) -> None: ...
+global___BTCScriptConfigRegistration = BTCScriptConfigRegistration
+
+class BTCSuccess(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    def __init__(self,
+        ) -> None: ...
+global___BTCSuccess = BTCSuccess
+
+class BTCIsScriptConfigRegisteredRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    REGISTRATION_FIELD_NUMBER: builtins.int
+    @property
+    def registration(self) -> global___BTCScriptConfigRegistration: ...
+    def __init__(self,
+        *,
+        registration: typing.Optional[global___BTCScriptConfigRegistration] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["registration",b"registration"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["registration",b"registration"]) -> None: ...
+global___BTCIsScriptConfigRegisteredRequest = BTCIsScriptConfigRegisteredRequest
+
+class BTCIsScriptConfigRegisteredResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    IS_REGISTERED_FIELD_NUMBER: builtins.int
+    is_registered: builtins.bool
+    def __init__(self,
+        *,
+        is_registered: builtins.bool = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["is_registered",b"is_registered"]) -> None: ...
+global___BTCIsScriptConfigRegisteredResponse = BTCIsScriptConfigRegisteredResponse
+
+class BTCRegisterScriptConfigRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _XPubType:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _XPubTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[BTCRegisterScriptConfigRequest._XPubType.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        AUTO_ELECTRUM: BTCRegisterScriptConfigRequest._XPubType.ValueType  # 0
+        """Automatically choose to match Electrum's xpub format (e.g. Zpub/Vpub for p2wsh multisig mainnet/testnet)."""
+
+        AUTO_XPUB_TPUB: BTCRegisterScriptConfigRequest._XPubType.ValueType  # 1
+        """Always xpub for mainnets, tpub for testnets."""
+
+    class XPubType(_XPubType, metaclass=_XPubTypeEnumTypeWrapper):
+        pass
+
+    AUTO_ELECTRUM: BTCRegisterScriptConfigRequest.XPubType.ValueType  # 0
+    """Automatically choose to match Electrum's xpub format (e.g. Zpub/Vpub for p2wsh multisig mainnet/testnet)."""
+
+    AUTO_XPUB_TPUB: BTCRegisterScriptConfigRequest.XPubType.ValueType  # 1
+    """Always xpub for mainnets, tpub for testnets."""
+
+
+    REGISTRATION_FIELD_NUMBER: builtins.int
+    NAME_FIELD_NUMBER: builtins.int
+    XPUB_TYPE_FIELD_NUMBER: builtins.int
+    @property
+    def registration(self) -> global___BTCScriptConfigRegistration: ...
+    name: typing.Text
+    """If empty, the name is entered on the device instead."""
+
+    xpub_type: global___BTCRegisterScriptConfigRequest.XPubType.ValueType
+    def __init__(self,
+        *,
+        registration: typing.Optional[global___BTCScriptConfigRegistration] = ...,
+        name: typing.Text = ...,
+        xpub_type: global___BTCRegisterScriptConfigRequest.XPubType.ValueType = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["registration",b"registration"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["name",b"name","registration",b"registration","xpub_type",b"xpub_type"]) -> None: ...
+global___BTCRegisterScriptConfigRequest = BTCRegisterScriptConfigRequest
+
+class BTCPrevTxInitRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    VERSION_FIELD_NUMBER: builtins.int
+    NUM_INPUTS_FIELD_NUMBER: builtins.int
+    NUM_OUTPUTS_FIELD_NUMBER: builtins.int
+    LOCKTIME_FIELD_NUMBER: builtins.int
+    version: builtins.int
+    num_inputs: builtins.int
+    num_outputs: builtins.int
+    locktime: builtins.int
+    def __init__(self,
+        *,
+        version: builtins.int = ...,
+        num_inputs: builtins.int = ...,
+        num_outputs: builtins.int = ...,
+        locktime: builtins.int = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["locktime",b"locktime","num_inputs",b"num_inputs","num_outputs",b"num_outputs","version",b"version"]) -> None: ...
+global___BTCPrevTxInitRequest = BTCPrevTxInitRequest
+
+class BTCPrevTxInputRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    PREV_OUT_HASH_FIELD_NUMBER: builtins.int
+    PREV_OUT_INDEX_FIELD_NUMBER: builtins.int
+    SIGNATURE_SCRIPT_FIELD_NUMBER: builtins.int
+    SEQUENCE_FIELD_NUMBER: builtins.int
+    prev_out_hash: builtins.bytes
+    prev_out_index: builtins.int
+    signature_script: builtins.bytes
+    sequence: builtins.int
+    def __init__(self,
+        *,
+        prev_out_hash: builtins.bytes = ...,
+        prev_out_index: builtins.int = ...,
+        signature_script: builtins.bytes = ...,
+        sequence: builtins.int = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["prev_out_hash",b"prev_out_hash","prev_out_index",b"prev_out_index","sequence",b"sequence","signature_script",b"signature_script"]) -> None: ...
+global___BTCPrevTxInputRequest = BTCPrevTxInputRequest
+
+class BTCPrevTxOutputRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    VALUE_FIELD_NUMBER: builtins.int
+    PUBKEY_SCRIPT_FIELD_NUMBER: builtins.int
+    value: builtins.int
+    pubkey_script: builtins.bytes
+    def __init__(self,
+        *,
+        value: builtins.int = ...,
+        pubkey_script: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["pubkey_script",b"pubkey_script","value",b"value"]) -> None: ...
+global___BTCPrevTxOutputRequest = BTCPrevTxOutputRequest
+
+class BTCSignMessageRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    COIN_FIELD_NUMBER: builtins.int
+    SCRIPT_CONFIG_FIELD_NUMBER: builtins.int
+    MSG_FIELD_NUMBER: builtins.int
+    HOST_NONCE_COMMITMENT_FIELD_NUMBER: builtins.int
+    coin: global___BTCCoin.ValueType
+    @property
+    def script_config(self) -> global___BTCScriptConfigWithKeypath: ...
+    msg: builtins.bytes
+    @property
+    def host_nonce_commitment(self) -> antiklepto_pb2.AntiKleptoHostNonceCommitment: ...
+    def __init__(self,
+        *,
+        coin: global___BTCCoin.ValueType = ...,
+        script_config: typing.Optional[global___BTCScriptConfigWithKeypath] = ...,
+        msg: builtins.bytes = ...,
+        host_nonce_commitment: typing.Optional[antiklepto_pb2.AntiKleptoHostNonceCommitment] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["host_nonce_commitment",b"host_nonce_commitment","script_config",b"script_config"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["coin",b"coin","host_nonce_commitment",b"host_nonce_commitment","msg",b"msg","script_config",b"script_config"]) -> None: ...
+global___BTCSignMessageRequest = BTCSignMessageRequest
+
+class BTCSignMessageResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    SIGNATURE_FIELD_NUMBER: builtins.int
+    signature: builtins.bytes
+    """65 bytes (32 bytes big endian R, 32 bytes big endian S, 1 recid)."""
+
+    def __init__(self,
+        *,
+        signature: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["signature",b"signature"]) -> None: ...
+global___BTCSignMessageResponse = BTCSignMessageResponse
+
+class BTCRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    IS_SCRIPT_CONFIG_REGISTERED_FIELD_NUMBER: builtins.int
+    REGISTER_SCRIPT_CONFIG_FIELD_NUMBER: builtins.int
+    PREVTX_INIT_FIELD_NUMBER: builtins.int
+    PREVTX_INPUT_FIELD_NUMBER: builtins.int
+    PREVTX_OUTPUT_FIELD_NUMBER: builtins.int
+    SIGN_MESSAGE_FIELD_NUMBER: builtins.int
+    ANTIKLEPTO_SIGNATURE_FIELD_NUMBER: builtins.int
+    @property
+    def is_script_config_registered(self) -> global___BTCIsScriptConfigRegisteredRequest: ...
+    @property
+    def register_script_config(self) -> global___BTCRegisterScriptConfigRequest: ...
+    @property
+    def prevtx_init(self) -> global___BTCPrevTxInitRequest: ...
+    @property
+    def prevtx_input(self) -> global___BTCPrevTxInputRequest: ...
+    @property
+    def prevtx_output(self) -> global___BTCPrevTxOutputRequest: ...
+    @property
+    def sign_message(self) -> global___BTCSignMessageRequest: ...
+    @property
+    def antiklepto_signature(self) -> antiklepto_pb2.AntiKleptoSignatureRequest: ...
+    def __init__(self,
+        *,
+        is_script_config_registered: typing.Optional[global___BTCIsScriptConfigRegisteredRequest] = ...,
+        register_script_config: typing.Optional[global___BTCRegisterScriptConfigRequest] = ...,
+        prevtx_init: typing.Optional[global___BTCPrevTxInitRequest] = ...,
+        prevtx_input: typing.Optional[global___BTCPrevTxInputRequest] = ...,
+        prevtx_output: typing.Optional[global___BTCPrevTxOutputRequest] = ...,
+        sign_message: typing.Optional[global___BTCSignMessageRequest] = ...,
+        antiklepto_signature: typing.Optional[antiklepto_pb2.AntiKleptoSignatureRequest] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["antiklepto_signature",b"antiklepto_signature","is_script_config_registered",b"is_script_config_registered","prevtx_init",b"prevtx_init","prevtx_input",b"prevtx_input","prevtx_output",b"prevtx_output","register_script_config",b"register_script_config","request",b"request","sign_message",b"sign_message"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["antiklepto_signature",b"antiklepto_signature","is_script_config_registered",b"is_script_config_registered","prevtx_init",b"prevtx_init","prevtx_input",b"prevtx_input","prevtx_output",b"prevtx_output","register_script_config",b"register_script_config","request",b"request","sign_message",b"sign_message"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["request",b"request"]) -> typing.Optional[typing_extensions.Literal["is_script_config_registered","register_script_config","prevtx_init","prevtx_input","prevtx_output","sign_message","antiklepto_signature"]]: ...
+global___BTCRequest = BTCRequest
+
+class BTCResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    SUCCESS_FIELD_NUMBER: builtins.int
+    IS_SCRIPT_CONFIG_REGISTERED_FIELD_NUMBER: builtins.int
+    SIGN_NEXT_FIELD_NUMBER: builtins.int
+    SIGN_MESSAGE_FIELD_NUMBER: builtins.int
+    ANTIKLEPTO_SIGNER_COMMITMENT_FIELD_NUMBER: builtins.int
+    @property
+    def success(self) -> global___BTCSuccess: ...
+    @property
+    def is_script_config_registered(self) -> global___BTCIsScriptConfigRegisteredResponse: ...
+    @property
+    def sign_next(self) -> global___BTCSignNextResponse: ...
+    @property
+    def sign_message(self) -> global___BTCSignMessageResponse: ...
+    @property
+    def antiklepto_signer_commitment(self) -> antiklepto_pb2.AntiKleptoSignerCommitment: ...
+    def __init__(self,
+        *,
+        success: typing.Optional[global___BTCSuccess] = ...,
+        is_script_config_registered: typing.Optional[global___BTCIsScriptConfigRegisteredResponse] = ...,
+        sign_next: typing.Optional[global___BTCSignNextResponse] = ...,
+        sign_message: typing.Optional[global___BTCSignMessageResponse] = ...,
+        antiklepto_signer_commitment: typing.Optional[antiklepto_pb2.AntiKleptoSignerCommitment] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["antiklepto_signer_commitment",b"antiklepto_signer_commitment","is_script_config_registered",b"is_script_config_registered","response",b"response","sign_message",b"sign_message","sign_next",b"sign_next","success",b"success"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["antiklepto_signer_commitment",b"antiklepto_signer_commitment","is_script_config_registered",b"is_script_config_registered","response",b"response","sign_message",b"sign_message","sign_next",b"sign_next","success",b"success"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["response",b"response"]) -> typing.Optional[typing_extensions.Literal["success","is_script_config_registered","sign_next","sign_message","antiklepto_signer_commitment"]]: ...
+global___BTCResponse = BTCResponse
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/cardano_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/cardano_pb2.py
new file mode 100644
index 000000000..ecacbeeef
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/cardano_pb2.py
@@ -0,0 +1,60 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: cardano.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from . import common_pb2 as common__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rcardano.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\"F\n\x13\x43\x61rdanoXpubsRequest\x12/\n\x08keypaths\x18\x01 \x03(\x0b\x32\x1d.shiftcrypto.bitbox02.Keypath\"%\n\x14\x43\x61rdanoXpubsResponse\x12\r\n\x05xpubs\x18\x01 \x03(\x0c\"\x9e\x01\n\x13\x43\x61rdanoScriptConfig\x12\x43\n\x07pkh_skh\x18\x01 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.CardanoScriptConfig.PkhSkhH\x00\x1a\x38\n\x06PkhSkh\x12\x17\n\x0fkeypath_payment\x18\x01 \x03(\r\x12\x15\n\rkeypath_stake\x18\x02 \x03(\rB\x08\n\x06\x63onfig\"\xa1\x01\n\x15\x43\x61rdanoAddressRequest\x12\x35\n\x07network\x18\x01 \x01(\x0e\x32$.shiftcrypto.bitbox02.CardanoNetwork\x12\x0f\n\x07\x64isplay\x18\x02 \x01(\x08\x12@\n\rscript_config\x18\x03 \x01(\x0b\x32).shiftcrypto.bitbox02.CardanoScriptConfig\"\x8e\n\n\x1d\x43\x61rdanoSignTransactionRequest\x12\x35\n\x07network\x18\x01 \x01(\x0e\x32$.shiftcrypto.bitbox02.CardanoNetwork\x12I\n\x06inputs\x18\x02 \x03(\x0b\x32\x39.shiftcrypto.bitbox02.CardanoSignTransactionRequest.Input\x12K\n\x07outputs\x18\x03 \x03(\x0b\x32:.shiftcrypto.bitbox02.CardanoSignTransactionRequest.Output\x12\x0b\n\x03\x66\x65\x65\x18\x04 \x01(\x04\x12\x0b\n\x03ttl\x18\x05 \x01(\x04\x12U\n\x0c\x63\x65rtificates\x18\x06 \x03(\x0b\x32?.shiftcrypto.bitbox02.CardanoSignTransactionRequest.Certificate\x12S\n\x0bwithdrawals\x18\x07 \x03(\x0b\x32>.shiftcrypto.bitbox02.CardanoSignTransactionRequest.Withdrawal\x12\x1f\n\x17validity_interval_start\x18\x08 \x01(\x04\x12\x16\n\x0e\x61llow_zero_ttl\x18\t \x01(\x08\x1aG\n\x05Input\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x12\x15\n\rprev_out_hash\x18\x02 \x01(\x0c\x12\x16\n\x0eprev_out_index\x18\x03 \x01(\r\x1a\xa1\x01\n\nAssetGroup\x12\x11\n\tpolicy_id\x18\x01 \x01(\x0c\x12T\n\x06tokens\x18\x02 \x03(\x0b\x32\x44.shiftcrypto.bitbox02.CardanoSignTransactionRequest.AssetGroup.Token\x1a*\n\x05Token\x12\x12\n\nasset_name\x18\x01 \x01(\x0c\x12\r\n\x05value\x18\x02 \x01(\x04\x1a\xc8\x01\n\x06Output\x12\x17\n\x0f\x65ncoded_address\x18\x01 \x01(\t\x12\r\n\x05value\x18\x02 \x01(\x04\x12@\n\rscript_config\x18\x03 \x01(\x0b\x32).shiftcrypto.bitbox02.CardanoScriptConfig\x12T\n\x0c\x61sset_groups\x18\x04 \x03(\x0b\x32>.shiftcrypto.bitbox02.CardanoSignTransactionRequest.AssetGroup\x1a\xb8\x02\n\x0b\x43\x65rtificate\x12;\n\x12stake_registration\x18\x01 \x01(\x0b\x32\x1d.shiftcrypto.bitbox02.KeypathH\x00\x12=\n\x14stake_deregistration\x18\x02 \x01(\x0b\x32\x1d.shiftcrypto.bitbox02.KeypathH\x00\x12k\n\x10stake_delegation\x18\x03 \x01(\x0b\x32O.shiftcrypto.bitbox02.CardanoSignTransactionRequest.Certificate.StakeDelegationH\x00\x1a\x38\n\x0fStakeDelegation\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x12\x14\n\x0cpool_keyhash\x18\x02 \x01(\x0c\x42\x06\n\x04\x63\x65rt\x1a,\n\nWithdrawal\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x12\r\n\x05value\x18\x02 \x01(\x04\"\xb9\x01\n\x1e\x43\x61rdanoSignTransactionResponse\x12^\n\x11shelley_witnesses\x18\x01 \x03(\x0b\x32\x43.shiftcrypto.bitbox02.CardanoSignTransactionResponse.ShelleyWitness\x1a\x37\n\x0eShelleyWitness\x12\x12\n\npublic_key\x18\x01 \x01(\x0c\x12\x11\n\tsignature\x18\x02 \x01(\x0c\"\xe8\x01\n\x0e\x43\x61rdanoRequest\x12:\n\x05xpubs\x18\x01 \x01(\x0b\x32).shiftcrypto.bitbox02.CardanoXpubsRequestH\x00\x12>\n\x07\x61\x64\x64ress\x18\x02 \x01(\x0b\x32+.shiftcrypto.bitbox02.CardanoAddressRequestH\x00\x12O\n\x10sign_transaction\x18\x03 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.CardanoSignTransactionRequestH\x00\x42\t\n\x07request\"\xde\x01\n\x0f\x43\x61rdanoResponse\x12;\n\x05xpubs\x18\x01 \x01(\x0b\x32*.shiftcrypto.bitbox02.CardanoXpubsResponseH\x00\x12\x30\n\x03pub\x18\x02 \x01(\x0b\x32!.shiftcrypto.bitbox02.PubResponseH\x00\x12P\n\x10sign_transaction\x18\x03 \x01(\x0b\x32\x34.shiftcrypto.bitbox02.CardanoSignTransactionResponseH\x00\x42\n\n\x08response*8\n\x0e\x43\x61rdanoNetwork\x12\x12\n\x0e\x43\x61rdanoMainnet\x10\x00\x12\x12\n\x0e\x43\x61rdanoTestnet\x10\x01\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cardano_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _CARDANONETWORK._serialized_start=2434
+  _CARDANONETWORK._serialized_end=2490
+  _CARDANOXPUBSREQUEST._serialized_start=53
+  _CARDANOXPUBSREQUEST._serialized_end=123
+  _CARDANOXPUBSRESPONSE._serialized_start=125
+  _CARDANOXPUBSRESPONSE._serialized_end=162
+  _CARDANOSCRIPTCONFIG._serialized_start=165
+  _CARDANOSCRIPTCONFIG._serialized_end=323
+  _CARDANOSCRIPTCONFIG_PKHSKH._serialized_start=257
+  _CARDANOSCRIPTCONFIG_PKHSKH._serialized_end=313
+  _CARDANOADDRESSREQUEST._serialized_start=326
+  _CARDANOADDRESSREQUEST._serialized_end=487
+  _CARDANOSIGNTRANSACTIONREQUEST._serialized_start=490
+  _CARDANOSIGNTRANSACTIONREQUEST._serialized_end=1784
+  _CARDANOSIGNTRANSACTIONREQUEST_INPUT._serialized_start=985
+  _CARDANOSIGNTRANSACTIONREQUEST_INPUT._serialized_end=1056
+  _CARDANOSIGNTRANSACTIONREQUEST_ASSETGROUP._serialized_start=1059
+  _CARDANOSIGNTRANSACTIONREQUEST_ASSETGROUP._serialized_end=1220
+  _CARDANOSIGNTRANSACTIONREQUEST_ASSETGROUP_TOKEN._serialized_start=1178
+  _CARDANOSIGNTRANSACTIONREQUEST_ASSETGROUP_TOKEN._serialized_end=1220
+  _CARDANOSIGNTRANSACTIONREQUEST_OUTPUT._serialized_start=1223
+  _CARDANOSIGNTRANSACTIONREQUEST_OUTPUT._serialized_end=1423
+  _CARDANOSIGNTRANSACTIONREQUEST_CERTIFICATE._serialized_start=1426
+  _CARDANOSIGNTRANSACTIONREQUEST_CERTIFICATE._serialized_end=1738
+  _CARDANOSIGNTRANSACTIONREQUEST_CERTIFICATE_STAKEDELEGATION._serialized_start=1674
+  _CARDANOSIGNTRANSACTIONREQUEST_CERTIFICATE_STAKEDELEGATION._serialized_end=1730
+  _CARDANOSIGNTRANSACTIONREQUEST_WITHDRAWAL._serialized_start=1740
+  _CARDANOSIGNTRANSACTIONREQUEST_WITHDRAWAL._serialized_end=1784
+  _CARDANOSIGNTRANSACTIONRESPONSE._serialized_start=1787
+  _CARDANOSIGNTRANSACTIONRESPONSE._serialized_end=1972
+  _CARDANOSIGNTRANSACTIONRESPONSE_SHELLEYWITNESS._serialized_start=1917
+  _CARDANOSIGNTRANSACTIONRESPONSE_SHELLEYWITNESS._serialized_end=1972
+  _CARDANOREQUEST._serialized_start=1975
+  _CARDANOREQUEST._serialized_end=2207
+  _CARDANORESPONSE._serialized_start=2210
+  _CARDANORESPONSE._serialized_end=2432
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/cardano_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/cardano_pb2.pyi
new file mode 100644
index 000000000..f85c4668a
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/cardano_pb2.pyi
@@ -0,0 +1,340 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+import builtins
+from . import common_pb2
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import typing
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class _CardanoNetwork:
+    ValueType = typing.NewType('ValueType', builtins.int)
+    V: typing_extensions.TypeAlias = ValueType
+class _CardanoNetworkEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_CardanoNetwork.ValueType], builtins.type):
+    DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+    CardanoMainnet: _CardanoNetwork.ValueType  # 0
+    CardanoTestnet: _CardanoNetwork.ValueType  # 1
+class CardanoNetwork(_CardanoNetwork, metaclass=_CardanoNetworkEnumTypeWrapper):
+    pass
+
+CardanoMainnet: CardanoNetwork.ValueType  # 0
+CardanoTestnet: CardanoNetwork.ValueType  # 1
+global___CardanoNetwork = CardanoNetwork
+
+
+class CardanoXpubsRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    KEYPATHS_FIELD_NUMBER: builtins.int
+    @property
+    def keypaths(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[common_pb2.Keypath]: ...
+    def __init__(self,
+        *,
+        keypaths: typing.Optional[typing.Iterable[common_pb2.Keypath]] = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["keypaths",b"keypaths"]) -> None: ...
+global___CardanoXpubsRequest = CardanoXpubsRequest
+
+class CardanoXpubsResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    XPUBS_FIELD_NUMBER: builtins.int
+    @property
+    def xpubs(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.bytes]: ...
+    def __init__(self,
+        *,
+        xpubs: typing.Optional[typing.Iterable[builtins.bytes]] = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["xpubs",b"xpubs"]) -> None: ...
+global___CardanoXpubsResponse = CardanoXpubsResponse
+
+class CardanoScriptConfig(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class PkhSkh(google.protobuf.message.Message):
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        KEYPATH_PAYMENT_FIELD_NUMBER: builtins.int
+        KEYPATH_STAKE_FIELD_NUMBER: builtins.int
+        @property
+        def keypath_payment(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+        @property
+        def keypath_stake(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+        def __init__(self,
+            *,
+            keypath_payment: typing.Optional[typing.Iterable[builtins.int]] = ...,
+            keypath_stake: typing.Optional[typing.Iterable[builtins.int]] = ...,
+            ) -> None: ...
+        def ClearField(self, field_name: typing_extensions.Literal["keypath_payment",b"keypath_payment","keypath_stake",b"keypath_stake"]) -> None: ...
+
+    PKH_SKH_FIELD_NUMBER: builtins.int
+    @property
+    def pkh_skh(self) -> global___CardanoScriptConfig.PkhSkh:
+        """Shelley PaymentKeyHash & StakeKeyHash"""
+        pass
+    def __init__(self,
+        *,
+        pkh_skh: typing.Optional[global___CardanoScriptConfig.PkhSkh] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["config",b"config","pkh_skh",b"pkh_skh"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["config",b"config","pkh_skh",b"pkh_skh"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["config",b"config"]) -> typing.Optional[typing_extensions.Literal["pkh_skh"]]: ...
+global___CardanoScriptConfig = CardanoScriptConfig
+
+class CardanoAddressRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    NETWORK_FIELD_NUMBER: builtins.int
+    DISPLAY_FIELD_NUMBER: builtins.int
+    SCRIPT_CONFIG_FIELD_NUMBER: builtins.int
+    network: global___CardanoNetwork.ValueType
+    display: builtins.bool
+    @property
+    def script_config(self) -> global___CardanoScriptConfig: ...
+    def __init__(self,
+        *,
+        network: global___CardanoNetwork.ValueType = ...,
+        display: builtins.bool = ...,
+        script_config: typing.Optional[global___CardanoScriptConfig] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["script_config",b"script_config"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["display",b"display","network",b"network","script_config",b"script_config"]) -> None: ...
+global___CardanoAddressRequest = CardanoAddressRequest
+
+class CardanoSignTransactionRequest(google.protobuf.message.Message):
+    """Max allowed transaction size is 16384 bytes according to
+    https://github.com/cardano-foundation/CIPs/blob/master/CIP-0009/CIP-0009.md. Unlike with BTC, we
+    can fit the whole request in RAM and don't need to stream.
+
+    See also: https://github.com/input-output-hk/cardano-ledger-specs/blob/d0aa86ded0b973b09b629e5aa62aa1e71364d088/eras/alonzo/test-suite/cddl-files/alonzo.cddl#L50
+    """
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class Input(google.protobuf.message.Message):
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        KEYPATH_FIELD_NUMBER: builtins.int
+        PREV_OUT_HASH_FIELD_NUMBER: builtins.int
+        PREV_OUT_INDEX_FIELD_NUMBER: builtins.int
+        @property
+        def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+        prev_out_hash: builtins.bytes
+        prev_out_index: builtins.int
+        def __init__(self,
+            *,
+            keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+            prev_out_hash: builtins.bytes = ...,
+            prev_out_index: builtins.int = ...,
+            ) -> None: ...
+        def ClearField(self, field_name: typing_extensions.Literal["keypath",b"keypath","prev_out_hash",b"prev_out_hash","prev_out_index",b"prev_out_index"]) -> None: ...
+
+    class AssetGroup(google.protobuf.message.Message):
+        """https://github.com/input-output-hk/cardano-ledger/blob/d0aa86ded0b973b09b629e5aa62aa1e71364d088/eras/alonzo/test-suite/cddl-files/alonzo.cddl#L358"""
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        class Token(google.protobuf.message.Message):
+            DESCRIPTOR: google.protobuf.descriptor.Descriptor
+            ASSET_NAME_FIELD_NUMBER: builtins.int
+            VALUE_FIELD_NUMBER: builtins.int
+            asset_name: builtins.bytes
+            value: builtins.int
+            """Number of tokens transacted of this asset."""
+
+            def __init__(self,
+                *,
+                asset_name: builtins.bytes = ...,
+                value: builtins.int = ...,
+                ) -> None: ...
+            def ClearField(self, field_name: typing_extensions.Literal["asset_name",b"asset_name","value",b"value"]) -> None: ...
+
+        POLICY_ID_FIELD_NUMBER: builtins.int
+        TOKENS_FIELD_NUMBER: builtins.int
+        policy_id: builtins.bytes
+        @property
+        def tokens(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CardanoSignTransactionRequest.AssetGroup.Token]: ...
+        def __init__(self,
+            *,
+            policy_id: builtins.bytes = ...,
+            tokens: typing.Optional[typing.Iterable[global___CardanoSignTransactionRequest.AssetGroup.Token]] = ...,
+            ) -> None: ...
+        def ClearField(self, field_name: typing_extensions.Literal["policy_id",b"policy_id","tokens",b"tokens"]) -> None: ...
+
+    class Output(google.protobuf.message.Message):
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        ENCODED_ADDRESS_FIELD_NUMBER: builtins.int
+        VALUE_FIELD_NUMBER: builtins.int
+        SCRIPT_CONFIG_FIELD_NUMBER: builtins.int
+        ASSET_GROUPS_FIELD_NUMBER: builtins.int
+        encoded_address: typing.Text
+        value: builtins.int
+        @property
+        def script_config(self) -> global___CardanoScriptConfig:
+            """Optional. If provided, this is validated as a change output."""
+            pass
+        @property
+        def asset_groups(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CardanoSignTransactionRequest.AssetGroup]: ...
+        def __init__(self,
+            *,
+            encoded_address: typing.Text = ...,
+            value: builtins.int = ...,
+            script_config: typing.Optional[global___CardanoScriptConfig] = ...,
+            asset_groups: typing.Optional[typing.Iterable[global___CardanoSignTransactionRequest.AssetGroup]] = ...,
+            ) -> None: ...
+        def HasField(self, field_name: typing_extensions.Literal["script_config",b"script_config"]) -> builtins.bool: ...
+        def ClearField(self, field_name: typing_extensions.Literal["asset_groups",b"asset_groups","encoded_address",b"encoded_address","script_config",b"script_config","value",b"value"]) -> None: ...
+
+    class Certificate(google.protobuf.message.Message):
+        """See https://github.com/input-output-hk/cardano-ledger-specs/blob/d0aa86ded0b973b09b629e5aa62aa1e71364d088/eras/alonzo/test-suite/cddl-files/alonzo.cddl#L150"""
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        class StakeDelegation(google.protobuf.message.Message):
+            DESCRIPTOR: google.protobuf.descriptor.Descriptor
+            KEYPATH_FIELD_NUMBER: builtins.int
+            POOL_KEYHASH_FIELD_NUMBER: builtins.int
+            @property
+            def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+            pool_keyhash: builtins.bytes
+            def __init__(self,
+                *,
+                keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+                pool_keyhash: builtins.bytes = ...,
+                ) -> None: ...
+            def ClearField(self, field_name: typing_extensions.Literal["keypath",b"keypath","pool_keyhash",b"pool_keyhash"]) -> None: ...
+
+        STAKE_REGISTRATION_FIELD_NUMBER: builtins.int
+        STAKE_DEREGISTRATION_FIELD_NUMBER: builtins.int
+        STAKE_DELEGATION_FIELD_NUMBER: builtins.int
+        @property
+        def stake_registration(self) -> common_pb2.Keypath: ...
+        @property
+        def stake_deregistration(self) -> common_pb2.Keypath: ...
+        @property
+        def stake_delegation(self) -> global___CardanoSignTransactionRequest.Certificate.StakeDelegation: ...
+        def __init__(self,
+            *,
+            stake_registration: typing.Optional[common_pb2.Keypath] = ...,
+            stake_deregistration: typing.Optional[common_pb2.Keypath] = ...,
+            stake_delegation: typing.Optional[global___CardanoSignTransactionRequest.Certificate.StakeDelegation] = ...,
+            ) -> None: ...
+        def HasField(self, field_name: typing_extensions.Literal["cert",b"cert","stake_delegation",b"stake_delegation","stake_deregistration",b"stake_deregistration","stake_registration",b"stake_registration"]) -> builtins.bool: ...
+        def ClearField(self, field_name: typing_extensions.Literal["cert",b"cert","stake_delegation",b"stake_delegation","stake_deregistration",b"stake_deregistration","stake_registration",b"stake_registration"]) -> None: ...
+        def WhichOneof(self, oneof_group: typing_extensions.Literal["cert",b"cert"]) -> typing.Optional[typing_extensions.Literal["stake_registration","stake_deregistration","stake_delegation"]]: ...
+
+    class Withdrawal(google.protobuf.message.Message):
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        KEYPATH_FIELD_NUMBER: builtins.int
+        VALUE_FIELD_NUMBER: builtins.int
+        @property
+        def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+        value: builtins.int
+        def __init__(self,
+            *,
+            keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+            value: builtins.int = ...,
+            ) -> None: ...
+        def ClearField(self, field_name: typing_extensions.Literal["keypath",b"keypath","value",b"value"]) -> None: ...
+
+    NETWORK_FIELD_NUMBER: builtins.int
+    INPUTS_FIELD_NUMBER: builtins.int
+    OUTPUTS_FIELD_NUMBER: builtins.int
+    FEE_FIELD_NUMBER: builtins.int
+    TTL_FIELD_NUMBER: builtins.int
+    CERTIFICATES_FIELD_NUMBER: builtins.int
+    WITHDRAWALS_FIELD_NUMBER: builtins.int
+    VALIDITY_INTERVAL_START_FIELD_NUMBER: builtins.int
+    ALLOW_ZERO_TTL_FIELD_NUMBER: builtins.int
+    network: global___CardanoNetwork.ValueType
+    @property
+    def inputs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CardanoSignTransactionRequest.Input]: ...
+    @property
+    def outputs(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CardanoSignTransactionRequest.Output]: ...
+    fee: builtins.int
+    ttl: builtins.int
+    @property
+    def certificates(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CardanoSignTransactionRequest.Certificate]: ...
+    @property
+    def withdrawals(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CardanoSignTransactionRequest.Withdrawal]: ...
+    validity_interval_start: builtins.int
+    allow_zero_ttl: builtins.bool
+    """include ttl even if it is zero"""
+
+    def __init__(self,
+        *,
+        network: global___CardanoNetwork.ValueType = ...,
+        inputs: typing.Optional[typing.Iterable[global___CardanoSignTransactionRequest.Input]] = ...,
+        outputs: typing.Optional[typing.Iterable[global___CardanoSignTransactionRequest.Output]] = ...,
+        fee: builtins.int = ...,
+        ttl: builtins.int = ...,
+        certificates: typing.Optional[typing.Iterable[global___CardanoSignTransactionRequest.Certificate]] = ...,
+        withdrawals: typing.Optional[typing.Iterable[global___CardanoSignTransactionRequest.Withdrawal]] = ...,
+        validity_interval_start: builtins.int = ...,
+        allow_zero_ttl: builtins.bool = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["allow_zero_ttl",b"allow_zero_ttl","certificates",b"certificates","fee",b"fee","inputs",b"inputs","network",b"network","outputs",b"outputs","ttl",b"ttl","validity_interval_start",b"validity_interval_start","withdrawals",b"withdrawals"]) -> None: ...
+global___CardanoSignTransactionRequest = CardanoSignTransactionRequest
+
+class CardanoSignTransactionResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class ShelleyWitness(google.protobuf.message.Message):
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        PUBLIC_KEY_FIELD_NUMBER: builtins.int
+        SIGNATURE_FIELD_NUMBER: builtins.int
+        public_key: builtins.bytes
+        signature: builtins.bytes
+        def __init__(self,
+            *,
+            public_key: builtins.bytes = ...,
+            signature: builtins.bytes = ...,
+            ) -> None: ...
+        def ClearField(self, field_name: typing_extensions.Literal["public_key",b"public_key","signature",b"signature"]) -> None: ...
+
+    SHELLEY_WITNESSES_FIELD_NUMBER: builtins.int
+    @property
+    def shelley_witnesses(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___CardanoSignTransactionResponse.ShelleyWitness]: ...
+    def __init__(self,
+        *,
+        shelley_witnesses: typing.Optional[typing.Iterable[global___CardanoSignTransactionResponse.ShelleyWitness]] = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["shelley_witnesses",b"shelley_witnesses"]) -> None: ...
+global___CardanoSignTransactionResponse = CardanoSignTransactionResponse
+
+class CardanoRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    XPUBS_FIELD_NUMBER: builtins.int
+    ADDRESS_FIELD_NUMBER: builtins.int
+    SIGN_TRANSACTION_FIELD_NUMBER: builtins.int
+    @property
+    def xpubs(self) -> global___CardanoXpubsRequest: ...
+    @property
+    def address(self) -> global___CardanoAddressRequest: ...
+    @property
+    def sign_transaction(self) -> global___CardanoSignTransactionRequest: ...
+    def __init__(self,
+        *,
+        xpubs: typing.Optional[global___CardanoXpubsRequest] = ...,
+        address: typing.Optional[global___CardanoAddressRequest] = ...,
+        sign_transaction: typing.Optional[global___CardanoSignTransactionRequest] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["address",b"address","request",b"request","sign_transaction",b"sign_transaction","xpubs",b"xpubs"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["address",b"address","request",b"request","sign_transaction",b"sign_transaction","xpubs",b"xpubs"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["request",b"request"]) -> typing.Optional[typing_extensions.Literal["xpubs","address","sign_transaction"]]: ...
+global___CardanoRequest = CardanoRequest
+
+class CardanoResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    XPUBS_FIELD_NUMBER: builtins.int
+    PUB_FIELD_NUMBER: builtins.int
+    SIGN_TRANSACTION_FIELD_NUMBER: builtins.int
+    @property
+    def xpubs(self) -> global___CardanoXpubsResponse: ...
+    @property
+    def pub(self) -> common_pb2.PubResponse: ...
+    @property
+    def sign_transaction(self) -> global___CardanoSignTransactionResponse: ...
+    def __init__(self,
+        *,
+        xpubs: typing.Optional[global___CardanoXpubsResponse] = ...,
+        pub: typing.Optional[common_pb2.PubResponse] = ...,
+        sign_transaction: typing.Optional[global___CardanoSignTransactionResponse] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["pub",b"pub","response",b"response","sign_transaction",b"sign_transaction","xpubs",b"xpubs"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["pub",b"pub","response",b"response","sign_transaction",b"sign_transaction","xpubs",b"xpubs"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["response",b"response"]) -> typing.Optional[typing_extensions.Literal["xpubs","pub","sign_transaction"]]: ...
+global___CardanoResponse = CardanoResponse
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/common_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/common_pb2.py
new file mode 100644
index 000000000..2da2b86d5
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/common_pb2.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: common.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0c\x63ommon.proto\x12\x14shiftcrypto.bitbox02\"\x1a\n\x0bPubResponse\x12\x0b\n\x03pub\x18\x01 \x01(\t\"\x18\n\x16RootFingerprintRequest\".\n\x17RootFingerprintResponse\x12\x13\n\x0b\x66ingerprint\x18\x01 \x01(\x0c\"l\n\x04XPub\x12\r\n\x05\x64\x65pth\x18\x01 \x01(\x0c\x12\x1a\n\x12parent_fingerprint\x18\x02 \x01(\x0c\x12\x11\n\tchild_num\x18\x03 \x01(\r\x12\x12\n\nchain_code\x18\x04 \x01(\x0c\x12\x12\n\npublic_key\x18\x05 \x01(\x0c\"\x1a\n\x07Keypath\x12\x0f\n\x07keypath\x18\x01 \x03(\rb\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'common_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _PUBRESPONSE._serialized_start=38
+  _PUBRESPONSE._serialized_end=64
+  _ROOTFINGERPRINTREQUEST._serialized_start=66
+  _ROOTFINGERPRINTREQUEST._serialized_end=90
+  _ROOTFINGERPRINTRESPONSE._serialized_start=92
+  _ROOTFINGERPRINTRESPONSE._serialized_end=138
+  _XPUB._serialized_start=140
+  _XPUB._serialized_end=248
+  _KEYPATH._serialized_start=250
+  _KEYPATH._serialized_end=276
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/common_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/common_pb2.pyi
new file mode 100644
index 000000000..adda12f50
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/common_pb2.pyi
@@ -0,0 +1,79 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+import builtins
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.message
+import typing
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class PubResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    PUB_FIELD_NUMBER: builtins.int
+    pub: typing.Text
+    def __init__(self,
+        *,
+        pub: typing.Text = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["pub",b"pub"]) -> None: ...
+global___PubResponse = PubResponse
+
+class RootFingerprintRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    def __init__(self,
+        ) -> None: ...
+global___RootFingerprintRequest = RootFingerprintRequest
+
+class RootFingerprintResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    FINGERPRINT_FIELD_NUMBER: builtins.int
+    fingerprint: builtins.bytes
+    def __init__(self,
+        *,
+        fingerprint: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["fingerprint",b"fingerprint"]) -> None: ...
+global___RootFingerprintResponse = RootFingerprintResponse
+
+class XPub(google.protobuf.message.Message):
+    """See https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki.
+    version field dropped as it will set dynamically based on the context (xpub, ypub, etc.).
+    """
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    DEPTH_FIELD_NUMBER: builtins.int
+    PARENT_FINGERPRINT_FIELD_NUMBER: builtins.int
+    CHILD_NUM_FIELD_NUMBER: builtins.int
+    CHAIN_CODE_FIELD_NUMBER: builtins.int
+    PUBLIC_KEY_FIELD_NUMBER: builtins.int
+    depth: builtins.bytes
+    parent_fingerprint: builtins.bytes
+    child_num: builtins.int
+    chain_code: builtins.bytes
+    public_key: builtins.bytes
+    def __init__(self,
+        *,
+        depth: builtins.bytes = ...,
+        parent_fingerprint: builtins.bytes = ...,
+        child_num: builtins.int = ...,
+        chain_code: builtins.bytes = ...,
+        public_key: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["chain_code",b"chain_code","child_num",b"child_num","depth",b"depth","parent_fingerprint",b"parent_fingerprint","public_key",b"public_key"]) -> None: ...
+global___XPub = XPub
+
+class Keypath(google.protobuf.message.Message):
+    """This message exists for use in oneof or repeated fields, where one can't inline `repeated uint32` due to protobuf rules."""
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    KEYPATH_FIELD_NUMBER: builtins.int
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    def __init__(self,
+        *,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["keypath",b"keypath"]) -> None: ...
+global___Keypath = Keypath
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/eth_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/eth_pb2.py
new file mode 100644
index 000000000..dde46019d
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/eth_pb2.py
@@ -0,0 +1,57 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: eth.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from . import common_pb2 as common__pb2
+from . import antiklepto_pb2 as antiklepto__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\teth.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x10\x61ntiklepto.proto\"\xf4\x01\n\rETHPubRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\x12+\n\x04\x63oin\x18\x02 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.ETHCoin\x12\x43\n\x0boutput_type\x18\x03 \x01(\x0e\x32..shiftcrypto.bitbox02.ETHPubRequest.OutputType\x12\x0f\n\x07\x64isplay\x18\x04 \x01(\x08\x12\x18\n\x10\x63ontract_address\x18\x05 \x01(\x0c\x12\x10\n\x08\x63hain_id\x18\x06 \x01(\x04\"#\n\nOutputType\x12\x0b\n\x07\x41\x44\x44RESS\x10\x00\x12\x08\n\x04XPUB\x10\x01\"\x99\x02\n\x0e\x45THSignRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.ETHCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\r\n\x05nonce\x18\x03 \x01(\x0c\x12\x11\n\tgas_price\x18\x04 \x01(\x0c\x12\x11\n\tgas_limit\x18\x05 \x01(\x0c\x12\x11\n\trecipient\x18\x06 \x01(\x0c\x12\r\n\x05value\x18\x07 \x01(\x0c\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\t \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x12\x10\n\x08\x63hain_id\x18\n \x01(\x04\"\xc8\x01\n\x15\x45THSignMessageRequest\x12+\n\x04\x63oin\x18\x01 \x01(\x0e\x32\x1d.shiftcrypto.bitbox02.ETHCoin\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12\x0b\n\x03msg\x18\x03 \x01(\x0c\x12R\n\x15host_nonce_commitment\x18\x04 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x12\x10\n\x08\x63hain_id\x18\x05 \x01(\x04\"$\n\x0f\x45THSignResponse\x12\x11\n\tsignature\x18\x01 \x01(\x0c\"\xfb\x05\n\x1a\x45THSignTypedMessageRequest\x12\x10\n\x08\x63hain_id\x18\x01 \x01(\x04\x12\x0f\n\x07keypath\x18\x02 \x03(\r\x12J\n\x05types\x18\x03 \x03(\x0b\x32;.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.StructType\x12\x14\n\x0cprimary_type\x18\x04 \x01(\t\x12R\n\x15host_nonce_commitment\x18\x05 \x01(\x0b\x32\x33.shiftcrypto.bitbox02.AntiKleptoHostNonceCommitment\x1a\xc9\x01\n\nMemberType\x12G\n\x04type\x18\x01 \x01(\x0e\x32\x39.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.DataType\x12\x0c\n\x04size\x18\x02 \x01(\r\x12\x13\n\x0bstruct_name\x18\x03 \x01(\t\x12O\n\narray_type\x18\x04 \x01(\x0b\x32;.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.MemberType\x1a\x61\n\x06Member\x12\x0c\n\x04name\x18\x01 \x01(\t\x12I\n\x04type\x18\x02 \x01(\x0b\x32;.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.MemberType\x1a\x64\n\nStructType\x12\x0c\n\x04name\x18\x01 \x01(\t\x12H\n\x07members\x18\x02 \x03(\x0b\x32\x37.shiftcrypto.bitbox02.ETHSignTypedMessageRequest.Member\"o\n\x08\x44\x61taType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\t\n\x05\x42YTES\x10\x01\x12\x08\n\x04UINT\x10\x02\x12\x07\n\x03INT\x10\x03\x12\x08\n\x04\x42OOL\x10\x04\x12\x0b\n\x07\x41\x44\x44RESS\x10\x05\x12\n\n\x06STRING\x10\x06\x12\t\n\x05\x41RRAY\x10\x07\x12\n\n\x06STRUCT\x10\x08\"\xb4\x01\n\x1c\x45THTypedMessageValueResponse\x12R\n\x0broot_object\x18\x01 \x01(\x0e\x32=.shiftcrypto.bitbox02.ETHTypedMessageValueResponse.RootObject\x12\x0c\n\x04path\x18\x02 \x03(\r\"2\n\nRootObject\x12\x0b\n\x07UNKNOWN\x10\x00\x12\n\n\x06\x44OMAIN\x10\x01\x12\x0b\n\x07MESSAGE\x10\x02\",\n\x1b\x45THTypedMessageValueRequest\x12\r\n\x05value\x18\x01 \x01(\x0c\"\xae\x03\n\nETHRequest\x12\x32\n\x03pub\x18\x01 \x01(\x0b\x32#.shiftcrypto.bitbox02.ETHPubRequestH\x00\x12\x34\n\x04sign\x18\x02 \x01(\x0b\x32$.shiftcrypto.bitbox02.ETHSignRequestH\x00\x12?\n\x08sign_msg\x18\x03 \x01(\x0b\x32+.shiftcrypto.bitbox02.ETHSignMessageRequestH\x00\x12P\n\x14\x61ntiklepto_signature\x18\x04 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignatureRequestH\x00\x12J\n\x0esign_typed_msg\x18\x05 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.ETHSignTypedMessageRequestH\x00\x12L\n\x0ftyped_msg_value\x18\x06 \x01(\x0b\x32\x31.shiftcrypto.bitbox02.ETHTypedMessageValueRequestH\x00\x42\t\n\x07request\"\xab\x02\n\x0b\x45THResponse\x12\x30\n\x03pub\x18\x01 \x01(\x0b\x32!.shiftcrypto.bitbox02.PubResponseH\x00\x12\x35\n\x04sign\x18\x02 \x01(\x0b\x32%.shiftcrypto.bitbox02.ETHSignResponseH\x00\x12X\n\x1c\x61ntiklepto_signer_commitment\x18\x03 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.AntiKleptoSignerCommitmentH\x00\x12M\n\x0ftyped_msg_value\x18\x04 \x01(\x0b\x32\x32.shiftcrypto.bitbox02.ETHTypedMessageValueResponseH\x00\x42\n\n\x08response*2\n\x07\x45THCoin\x12\x07\n\x03\x45TH\x10\x00\x12\x0e\n\nRopstenETH\x10\x01\x12\x0e\n\nRinkebyETH\x10\x02\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'eth_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _ETHCOIN._serialized_start=2569
+  _ETHCOIN._serialized_end=2619
+  _ETHPUBREQUEST._serialized_start=68
+  _ETHPUBREQUEST._serialized_end=312
+  _ETHPUBREQUEST_OUTPUTTYPE._serialized_start=277
+  _ETHPUBREQUEST_OUTPUTTYPE._serialized_end=312
+  _ETHSIGNREQUEST._serialized_start=315
+  _ETHSIGNREQUEST._serialized_end=596
+  _ETHSIGNMESSAGEREQUEST._serialized_start=599
+  _ETHSIGNMESSAGEREQUEST._serialized_end=799
+  _ETHSIGNRESPONSE._serialized_start=801
+  _ETHSIGNRESPONSE._serialized_end=837
+  _ETHSIGNTYPEDMESSAGEREQUEST._serialized_start=840
+  _ETHSIGNTYPEDMESSAGEREQUEST._serialized_end=1603
+  _ETHSIGNTYPEDMESSAGEREQUEST_MEMBERTYPE._serialized_start=1088
+  _ETHSIGNTYPEDMESSAGEREQUEST_MEMBERTYPE._serialized_end=1289
+  _ETHSIGNTYPEDMESSAGEREQUEST_MEMBER._serialized_start=1291
+  _ETHSIGNTYPEDMESSAGEREQUEST_MEMBER._serialized_end=1388
+  _ETHSIGNTYPEDMESSAGEREQUEST_STRUCTTYPE._serialized_start=1390
+  _ETHSIGNTYPEDMESSAGEREQUEST_STRUCTTYPE._serialized_end=1490
+  _ETHSIGNTYPEDMESSAGEREQUEST_DATATYPE._serialized_start=1492
+  _ETHSIGNTYPEDMESSAGEREQUEST_DATATYPE._serialized_end=1603
+  _ETHTYPEDMESSAGEVALUERESPONSE._serialized_start=1606
+  _ETHTYPEDMESSAGEVALUERESPONSE._serialized_end=1786
+  _ETHTYPEDMESSAGEVALUERESPONSE_ROOTOBJECT._serialized_start=1736
+  _ETHTYPEDMESSAGEVALUERESPONSE_ROOTOBJECT._serialized_end=1786
+  _ETHTYPEDMESSAGEVALUEREQUEST._serialized_start=1788
+  _ETHTYPEDMESSAGEVALUEREQUEST._serialized_end=1832
+  _ETHREQUEST._serialized_start=1835
+  _ETHREQUEST._serialized_end=2265
+  _ETHRESPONSE._serialized_start=2268
+  _ETHRESPONSE._serialized_end=2567
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/eth_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/eth_pb2.pyi
new file mode 100644
index 000000000..b1498cf73
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/eth_pb2.pyi
@@ -0,0 +1,392 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+from . import antiklepto_pb2
+import builtins
+from . import common_pb2
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import typing
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class _ETHCoin:
+    ValueType = typing.NewType('ValueType', builtins.int)
+    V: typing_extensions.TypeAlias = ValueType
+class _ETHCoinEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[_ETHCoin.ValueType], builtins.type):
+    DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+    ETH: _ETHCoin.ValueType  # 0
+    RopstenETH: _ETHCoin.ValueType  # 1
+    """Removed in v9.14.0 - deprecated"""
+
+    RinkebyETH: _ETHCoin.ValueType  # 2
+    """Removed in v9.14.0 - deprecated"""
+
+class ETHCoin(_ETHCoin, metaclass=_ETHCoinEnumTypeWrapper):
+    """Kept for backwards compatibility. Use chain_id instead, introduced in v9.10.0."""
+    pass
+
+ETH: ETHCoin.ValueType  # 0
+RopstenETH: ETHCoin.ValueType  # 1
+"""Removed in v9.14.0 - deprecated"""
+
+RinkebyETH: ETHCoin.ValueType  # 2
+"""Removed in v9.14.0 - deprecated"""
+
+global___ETHCoin = ETHCoin
+
+
+class ETHPubRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _OutputType:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _OutputTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ETHPubRequest._OutputType.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        ADDRESS: ETHPubRequest._OutputType.ValueType  # 0
+        XPUB: ETHPubRequest._OutputType.ValueType  # 1
+    class OutputType(_OutputType, metaclass=_OutputTypeEnumTypeWrapper):
+        pass
+
+    ADDRESS: ETHPubRequest.OutputType.ValueType  # 0
+    XPUB: ETHPubRequest.OutputType.ValueType  # 1
+
+    KEYPATH_FIELD_NUMBER: builtins.int
+    COIN_FIELD_NUMBER: builtins.int
+    OUTPUT_TYPE_FIELD_NUMBER: builtins.int
+    DISPLAY_FIELD_NUMBER: builtins.int
+    CONTRACT_ADDRESS_FIELD_NUMBER: builtins.int
+    CHAIN_ID_FIELD_NUMBER: builtins.int
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    coin: global___ETHCoin.ValueType
+    """Deprecated: use chain_id instead."""
+
+    output_type: global___ETHPubRequest.OutputType.ValueType
+    display: builtins.bool
+    contract_address: builtins.bytes
+    chain_id: builtins.int
+    """If non-zero, `coin` is ignored and `chain_id` is used to identify the network."""
+
+    def __init__(self,
+        *,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        coin: global___ETHCoin.ValueType = ...,
+        output_type: global___ETHPubRequest.OutputType.ValueType = ...,
+        display: builtins.bool = ...,
+        contract_address: builtins.bytes = ...,
+        chain_id: builtins.int = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["chain_id",b"chain_id","coin",b"coin","contract_address",b"contract_address","display",b"display","keypath",b"keypath","output_type",b"output_type"]) -> None: ...
+global___ETHPubRequest = ETHPubRequest
+
+class ETHSignRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    COIN_FIELD_NUMBER: builtins.int
+    KEYPATH_FIELD_NUMBER: builtins.int
+    NONCE_FIELD_NUMBER: builtins.int
+    GAS_PRICE_FIELD_NUMBER: builtins.int
+    GAS_LIMIT_FIELD_NUMBER: builtins.int
+    RECIPIENT_FIELD_NUMBER: builtins.int
+    VALUE_FIELD_NUMBER: builtins.int
+    DATA_FIELD_NUMBER: builtins.int
+    HOST_NONCE_COMMITMENT_FIELD_NUMBER: builtins.int
+    CHAIN_ID_FIELD_NUMBER: builtins.int
+    coin: global___ETHCoin.ValueType
+    """Deprecated: use chain_id instead."""
+
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    nonce: builtins.bytes
+    """smallest big endian serialization, max. 16 bytes"""
+
+    gas_price: builtins.bytes
+    """smallest big endian serialization, max. 16 bytes"""
+
+    gas_limit: builtins.bytes
+    """smallest big endian serialization, max. 16 bytes"""
+
+    recipient: builtins.bytes
+    """20 byte recipient"""
+
+    value: builtins.bytes
+    """smallest big endian serialization, max. 32 bytes"""
+
+    data: builtins.bytes
+    @property
+    def host_nonce_commitment(self) -> antiklepto_pb2.AntiKleptoHostNonceCommitment: ...
+    chain_id: builtins.int
+    """If non-zero, `coin` is ignored and `chain_id` is used to identify the network."""
+
+    def __init__(self,
+        *,
+        coin: global___ETHCoin.ValueType = ...,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        nonce: builtins.bytes = ...,
+        gas_price: builtins.bytes = ...,
+        gas_limit: builtins.bytes = ...,
+        recipient: builtins.bytes = ...,
+        value: builtins.bytes = ...,
+        data: builtins.bytes = ...,
+        host_nonce_commitment: typing.Optional[antiklepto_pb2.AntiKleptoHostNonceCommitment] = ...,
+        chain_id: builtins.int = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["host_nonce_commitment",b"host_nonce_commitment"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["chain_id",b"chain_id","coin",b"coin","data",b"data","gas_limit",b"gas_limit","gas_price",b"gas_price","host_nonce_commitment",b"host_nonce_commitment","keypath",b"keypath","nonce",b"nonce","recipient",b"recipient","value",b"value"]) -> None: ...
+global___ETHSignRequest = ETHSignRequest
+
+class ETHSignMessageRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    COIN_FIELD_NUMBER: builtins.int
+    KEYPATH_FIELD_NUMBER: builtins.int
+    MSG_FIELD_NUMBER: builtins.int
+    HOST_NONCE_COMMITMENT_FIELD_NUMBER: builtins.int
+    CHAIN_ID_FIELD_NUMBER: builtins.int
+    coin: global___ETHCoin.ValueType
+    """Deprecated: use chain_id instead."""
+
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    msg: builtins.bytes
+    @property
+    def host_nonce_commitment(self) -> antiklepto_pb2.AntiKleptoHostNonceCommitment: ...
+    chain_id: builtins.int
+    """If non-zero, `coin` is ignored and `chain_id` is used to identify the network."""
+
+    def __init__(self,
+        *,
+        coin: global___ETHCoin.ValueType = ...,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        msg: builtins.bytes = ...,
+        host_nonce_commitment: typing.Optional[antiklepto_pb2.AntiKleptoHostNonceCommitment] = ...,
+        chain_id: builtins.int = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["host_nonce_commitment",b"host_nonce_commitment"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["chain_id",b"chain_id","coin",b"coin","host_nonce_commitment",b"host_nonce_commitment","keypath",b"keypath","msg",b"msg"]) -> None: ...
+global___ETHSignMessageRequest = ETHSignMessageRequest
+
+class ETHSignResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    SIGNATURE_FIELD_NUMBER: builtins.int
+    signature: builtins.bytes
+    """65 bytes, last byte is the recid"""
+
+    def __init__(self,
+        *,
+        signature: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["signature",b"signature"]) -> None: ...
+global___ETHSignResponse = ETHSignResponse
+
+class ETHSignTypedMessageRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _DataType:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _DataTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ETHSignTypedMessageRequest._DataType.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        UNKNOWN: ETHSignTypedMessageRequest._DataType.ValueType  # 0
+        BYTES: ETHSignTypedMessageRequest._DataType.ValueType  # 1
+        UINT: ETHSignTypedMessageRequest._DataType.ValueType  # 2
+        INT: ETHSignTypedMessageRequest._DataType.ValueType  # 3
+        BOOL: ETHSignTypedMessageRequest._DataType.ValueType  # 4
+        ADDRESS: ETHSignTypedMessageRequest._DataType.ValueType  # 5
+        STRING: ETHSignTypedMessageRequest._DataType.ValueType  # 6
+        ARRAY: ETHSignTypedMessageRequest._DataType.ValueType  # 7
+        STRUCT: ETHSignTypedMessageRequest._DataType.ValueType  # 8
+    class DataType(_DataType, metaclass=_DataTypeEnumTypeWrapper):
+        pass
+
+    UNKNOWN: ETHSignTypedMessageRequest.DataType.ValueType  # 0
+    BYTES: ETHSignTypedMessageRequest.DataType.ValueType  # 1
+    UINT: ETHSignTypedMessageRequest.DataType.ValueType  # 2
+    INT: ETHSignTypedMessageRequest.DataType.ValueType  # 3
+    BOOL: ETHSignTypedMessageRequest.DataType.ValueType  # 4
+    ADDRESS: ETHSignTypedMessageRequest.DataType.ValueType  # 5
+    STRING: ETHSignTypedMessageRequest.DataType.ValueType  # 6
+    ARRAY: ETHSignTypedMessageRequest.DataType.ValueType  # 7
+    STRUCT: ETHSignTypedMessageRequest.DataType.ValueType  # 8
+
+    class MemberType(google.protobuf.message.Message):
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        TYPE_FIELD_NUMBER: builtins.int
+        SIZE_FIELD_NUMBER: builtins.int
+        STRUCT_NAME_FIELD_NUMBER: builtins.int
+        ARRAY_TYPE_FIELD_NUMBER: builtins.int
+        type: global___ETHSignTypedMessageRequest.DataType.ValueType
+        size: builtins.int
+        struct_name: typing.Text
+        """if type==STRUCT, name of struct type."""
+
+        @property
+        def array_type(self) -> global___ETHSignTypedMessageRequest.MemberType:
+            """if type==ARRAY, type of elements"""
+            pass
+        def __init__(self,
+            *,
+            type: global___ETHSignTypedMessageRequest.DataType.ValueType = ...,
+            size: builtins.int = ...,
+            struct_name: typing.Text = ...,
+            array_type: typing.Optional[global___ETHSignTypedMessageRequest.MemberType] = ...,
+            ) -> None: ...
+        def HasField(self, field_name: typing_extensions.Literal["array_type",b"array_type"]) -> builtins.bool: ...
+        def ClearField(self, field_name: typing_extensions.Literal["array_type",b"array_type","size",b"size","struct_name",b"struct_name","type",b"type"]) -> None: ...
+
+    class Member(google.protobuf.message.Message):
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        NAME_FIELD_NUMBER: builtins.int
+        TYPE_FIELD_NUMBER: builtins.int
+        name: typing.Text
+        @property
+        def type(self) -> global___ETHSignTypedMessageRequest.MemberType: ...
+        def __init__(self,
+            *,
+            name: typing.Text = ...,
+            type: typing.Optional[global___ETHSignTypedMessageRequest.MemberType] = ...,
+            ) -> None: ...
+        def HasField(self, field_name: typing_extensions.Literal["type",b"type"]) -> builtins.bool: ...
+        def ClearField(self, field_name: typing_extensions.Literal["name",b"name","type",b"type"]) -> None: ...
+
+    class StructType(google.protobuf.message.Message):
+        DESCRIPTOR: google.protobuf.descriptor.Descriptor
+        NAME_FIELD_NUMBER: builtins.int
+        MEMBERS_FIELD_NUMBER: builtins.int
+        name: typing.Text
+        @property
+        def members(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ETHSignTypedMessageRequest.Member]: ...
+        def __init__(self,
+            *,
+            name: typing.Text = ...,
+            members: typing.Optional[typing.Iterable[global___ETHSignTypedMessageRequest.Member]] = ...,
+            ) -> None: ...
+        def ClearField(self, field_name: typing_extensions.Literal["members",b"members","name",b"name"]) -> None: ...
+
+    CHAIN_ID_FIELD_NUMBER: builtins.int
+    KEYPATH_FIELD_NUMBER: builtins.int
+    TYPES_FIELD_NUMBER: builtins.int
+    PRIMARY_TYPE_FIELD_NUMBER: builtins.int
+    HOST_NONCE_COMMITMENT_FIELD_NUMBER: builtins.int
+    chain_id: builtins.int
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    @property
+    def types(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___ETHSignTypedMessageRequest.StructType]: ...
+    primary_type: typing.Text
+    @property
+    def host_nonce_commitment(self) -> antiklepto_pb2.AntiKleptoHostNonceCommitment: ...
+    def __init__(self,
+        *,
+        chain_id: builtins.int = ...,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        types: typing.Optional[typing.Iterable[global___ETHSignTypedMessageRequest.StructType]] = ...,
+        primary_type: typing.Text = ...,
+        host_nonce_commitment: typing.Optional[antiklepto_pb2.AntiKleptoHostNonceCommitment] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["host_nonce_commitment",b"host_nonce_commitment"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["chain_id",b"chain_id","host_nonce_commitment",b"host_nonce_commitment","keypath",b"keypath","primary_type",b"primary_type","types",b"types"]) -> None: ...
+global___ETHSignTypedMessageRequest = ETHSignTypedMessageRequest
+
+class ETHTypedMessageValueResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _RootObject:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _RootObjectEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[ETHTypedMessageValueResponse._RootObject.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        UNKNOWN: ETHTypedMessageValueResponse._RootObject.ValueType  # 0
+        DOMAIN: ETHTypedMessageValueResponse._RootObject.ValueType  # 1
+        MESSAGE: ETHTypedMessageValueResponse._RootObject.ValueType  # 2
+    class RootObject(_RootObject, metaclass=_RootObjectEnumTypeWrapper):
+        pass
+
+    UNKNOWN: ETHTypedMessageValueResponse.RootObject.ValueType  # 0
+    DOMAIN: ETHTypedMessageValueResponse.RootObject.ValueType  # 1
+    MESSAGE: ETHTypedMessageValueResponse.RootObject.ValueType  # 2
+
+    ROOT_OBJECT_FIELD_NUMBER: builtins.int
+    PATH_FIELD_NUMBER: builtins.int
+    root_object: global___ETHTypedMessageValueResponse.RootObject.ValueType
+    @property
+    def path(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    def __init__(self,
+        *,
+        root_object: global___ETHTypedMessageValueResponse.RootObject.ValueType = ...,
+        path: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["path",b"path","root_object",b"root_object"]) -> None: ...
+global___ETHTypedMessageValueResponse = ETHTypedMessageValueResponse
+
+class ETHTypedMessageValueRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    VALUE_FIELD_NUMBER: builtins.int
+    value: builtins.bytes
+    def __init__(self,
+        *,
+        value: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["value",b"value"]) -> None: ...
+global___ETHTypedMessageValueRequest = ETHTypedMessageValueRequest
+
+class ETHRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    PUB_FIELD_NUMBER: builtins.int
+    SIGN_FIELD_NUMBER: builtins.int
+    SIGN_MSG_FIELD_NUMBER: builtins.int
+    ANTIKLEPTO_SIGNATURE_FIELD_NUMBER: builtins.int
+    SIGN_TYPED_MSG_FIELD_NUMBER: builtins.int
+    TYPED_MSG_VALUE_FIELD_NUMBER: builtins.int
+    @property
+    def pub(self) -> global___ETHPubRequest: ...
+    @property
+    def sign(self) -> global___ETHSignRequest: ...
+    @property
+    def sign_msg(self) -> global___ETHSignMessageRequest: ...
+    @property
+    def antiklepto_signature(self) -> antiklepto_pb2.AntiKleptoSignatureRequest: ...
+    @property
+    def sign_typed_msg(self) -> global___ETHSignTypedMessageRequest: ...
+    @property
+    def typed_msg_value(self) -> global___ETHTypedMessageValueRequest: ...
+    def __init__(self,
+        *,
+        pub: typing.Optional[global___ETHPubRequest] = ...,
+        sign: typing.Optional[global___ETHSignRequest] = ...,
+        sign_msg: typing.Optional[global___ETHSignMessageRequest] = ...,
+        antiklepto_signature: typing.Optional[antiklepto_pb2.AntiKleptoSignatureRequest] = ...,
+        sign_typed_msg: typing.Optional[global___ETHSignTypedMessageRequest] = ...,
+        typed_msg_value: typing.Optional[global___ETHTypedMessageValueRequest] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["antiklepto_signature",b"antiklepto_signature","pub",b"pub","request",b"request","sign",b"sign","sign_msg",b"sign_msg","sign_typed_msg",b"sign_typed_msg","typed_msg_value",b"typed_msg_value"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["antiklepto_signature",b"antiklepto_signature","pub",b"pub","request",b"request","sign",b"sign","sign_msg",b"sign_msg","sign_typed_msg",b"sign_typed_msg","typed_msg_value",b"typed_msg_value"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["request",b"request"]) -> typing.Optional[typing_extensions.Literal["pub","sign","sign_msg","antiklepto_signature","sign_typed_msg","typed_msg_value"]]: ...
+global___ETHRequest = ETHRequest
+
+class ETHResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    PUB_FIELD_NUMBER: builtins.int
+    SIGN_FIELD_NUMBER: builtins.int
+    ANTIKLEPTO_SIGNER_COMMITMENT_FIELD_NUMBER: builtins.int
+    TYPED_MSG_VALUE_FIELD_NUMBER: builtins.int
+    @property
+    def pub(self) -> common_pb2.PubResponse: ...
+    @property
+    def sign(self) -> global___ETHSignResponse: ...
+    @property
+    def antiklepto_signer_commitment(self) -> antiklepto_pb2.AntiKleptoSignerCommitment: ...
+    @property
+    def typed_msg_value(self) -> global___ETHTypedMessageValueResponse: ...
+    def __init__(self,
+        *,
+        pub: typing.Optional[common_pb2.PubResponse] = ...,
+        sign: typing.Optional[global___ETHSignResponse] = ...,
+        antiklepto_signer_commitment: typing.Optional[antiklepto_pb2.AntiKleptoSignerCommitment] = ...,
+        typed_msg_value: typing.Optional[global___ETHTypedMessageValueResponse] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["antiklepto_signer_commitment",b"antiklepto_signer_commitment","pub",b"pub","response",b"response","sign",b"sign","typed_msg_value",b"typed_msg_value"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["antiklepto_signer_commitment",b"antiklepto_signer_commitment","pub",b"pub","response",b"response","sign",b"sign","typed_msg_value",b"typed_msg_value"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["response",b"response"]) -> typing.Optional[typing_extensions.Literal["pub","sign","antiklepto_signer_commitment","typed_msg_value"]]: ...
+global___ETHResponse = ETHResponse
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/hww_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/hww_pb2.py
new file mode 100644
index 000000000..8d6efa0a2
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/hww_pb2.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: hww.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from . import common_pb2 as common__pb2
+from . import backup_commands_pb2 as backup__commands__pb2
+from . import bitbox02_system_pb2 as bitbox02__system__pb2
+from . import btc_pb2 as btc__pb2
+from . import cardano_pb2 as cardano__pb2
+from . import eth_pb2 as eth__pb2
+from . import keystore_pb2 as keystore__pb2
+from . import mnemonic_pb2 as mnemonic__pb2
+from . import system_pb2 as system__pb2
+from . import perform_attestation_pb2 as perform__attestation__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\thww.proto\x12\x14shiftcrypto.bitbox02\x1a\x0c\x63ommon.proto\x1a\x15\x62\x61\x63kup_commands.proto\x1a\x15\x62itbox02_system.proto\x1a\tbtc.proto\x1a\rcardano.proto\x1a\teth.proto\x1a\x0ekeystore.proto\x1a\x0emnemonic.proto\x1a\x0csystem.proto\x1a\x19perform_attestation.proto\"&\n\x05\x45rror\x12\x0c\n\x04\x63ode\x18\x01 \x01(\x05\x12\x0f\n\x07message\x18\x02 \x01(\t\"\t\n\x07Success\"\xc8\r\n\x07Request\x12\x41\n\x0b\x64\x65vice_name\x18\x02 \x01(\x0b\x32*.shiftcrypto.bitbox02.SetDeviceNameRequestH\x00\x12I\n\x0f\x64\x65vice_language\x18\x03 \x01(\x0b\x32..shiftcrypto.bitbox02.SetDeviceLanguageRequestH\x00\x12>\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32\'.shiftcrypto.bitbox02.DeviceInfoRequestH\x00\x12@\n\x0cset_password\x18\x05 \x01(\x0b\x32(.shiftcrypto.bitbox02.SetPasswordRequestH\x00\x12\x42\n\rcreate_backup\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.CreateBackupRequestH\x00\x12\x42\n\rshow_mnemonic\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ShowMnemonicRequestH\x00\x12\x36\n\x07\x62tc_pub\x18\x08 \x01(\x0b\x32#.shiftcrypto.bitbox02.BTCPubRequestH\x00\x12\x41\n\rbtc_sign_init\x18\t \x01(\x0b\x32(.shiftcrypto.bitbox02.BTCSignInitRequestH\x00\x12\x43\n\x0e\x62tc_sign_input\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignInputRequestH\x00\x12\x45\n\x0f\x62tc_sign_output\x18\x0b \x01(\x0b\x32*.shiftcrypto.bitbox02.BTCSignOutputRequestH\x00\x12O\n\x14insert_remove_sdcard\x18\x0c \x01(\x0b\x32/.shiftcrypto.bitbox02.InsertRemoveSDCardRequestH\x00\x12@\n\x0c\x63heck_sdcard\x18\r \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckSDCardRequestH\x00\x12\x64\n\x1fset_mnemonic_passphrase_enabled\x18\x0e \x01(\x0b\x32\x39.shiftcrypto.bitbox02.SetMnemonicPassphraseEnabledRequestH\x00\x12@\n\x0clist_backups\x18\x0f \x01(\x0b\x32(.shiftcrypto.bitbox02.ListBackupsRequestH\x00\x12\x44\n\x0erestore_backup\x18\x10 \x01(\x0b\x32*.shiftcrypto.bitbox02.RestoreBackupRequestH\x00\x12N\n\x13perform_attestation\x18\x11 \x01(\x0b\x32/.shiftcrypto.bitbox02.PerformAttestationRequestH\x00\x12\x35\n\x06reboot\x18\x12 \x01(\x0b\x32#.shiftcrypto.bitbox02.RebootRequestH\x00\x12@\n\x0c\x63heck_backup\x18\x13 \x01(\x0b\x32(.shiftcrypto.bitbox02.CheckBackupRequestH\x00\x12/\n\x03\x65th\x18\x14 \x01(\x0b\x32 .shiftcrypto.bitbox02.ETHRequestH\x00\x12\x33\n\x05reset\x18\x15 \x01(\x0b\x32\".shiftcrypto.bitbox02.ResetRequestH\x00\x12Q\n\x15restore_from_mnemonic\x18\x16 \x01(\x0b\x32\x30.shiftcrypto.bitbox02.RestoreFromMnemonicRequestH\x00\x12\x43\n\x0b\x66ingerprint\x18\x18 \x01(\x0b\x32,.shiftcrypto.bitbox02.RootFingerprintRequestH\x00\x12/\n\x03\x62tc\x18\x19 \x01(\x0b\x32 .shiftcrypto.bitbox02.BTCRequestH\x00\x12U\n\x17\x65lectrum_encryption_key\x18\x1a \x01(\x0b\x32\x32.shiftcrypto.bitbox02.ElectrumEncryptionKeyRequestH\x00\x12\x37\n\x07\x63\x61rdano\x18\x1b \x01(\x0b\x32$.shiftcrypto.bitbox02.CardanoRequestH\x00\x42\t\n\x07requestJ\x04\x08\x01\x10\x02J\x04\x08\x17\x10\x18\"\x89\x07\n\x08Response\x12\x30\n\x07success\x18\x01 \x01(\x0b\x32\x1d.shiftcrypto.bitbox02.SuccessH\x00\x12,\n\x05\x65rror\x18\x02 \x01(\x0b\x32\x1b.shiftcrypto.bitbox02.ErrorH\x00\x12?\n\x0b\x64\x65vice_info\x18\x04 \x01(\x0b\x32(.shiftcrypto.bitbox02.DeviceInfoResponseH\x00\x12\x30\n\x03pub\x18\x05 \x01(\x0b\x32!.shiftcrypto.bitbox02.PubResponseH\x00\x12\x42\n\rbtc_sign_next\x18\x06 \x01(\x0b\x32).shiftcrypto.bitbox02.BTCSignNextResponseH\x00\x12\x41\n\x0clist_backups\x18\x07 \x01(\x0b\x32).shiftcrypto.bitbox02.ListBackupsResponseH\x00\x12\x41\n\x0c\x63heck_backup\x18\x08 \x01(\x0b\x32).shiftcrypto.bitbox02.CheckBackupResponseH\x00\x12O\n\x13perform_attestation\x18\t \x01(\x0b\x32\x30.shiftcrypto.bitbox02.PerformAttestationResponseH\x00\x12\x41\n\x0c\x63heck_sdcard\x18\n \x01(\x0b\x32).shiftcrypto.bitbox02.CheckSDCardResponseH\x00\x12\x30\n\x03\x65th\x18\x0b \x01(\x0b\x32!.shiftcrypto.bitbox02.ETHResponseH\x00\x12\x44\n\x0b\x66ingerprint\x18\x0c \x01(\x0b\x32-.shiftcrypto.bitbox02.RootFingerprintResponseH\x00\x12\x30\n\x03\x62tc\x18\r \x01(\x0b\x32!.shiftcrypto.bitbox02.BTCResponseH\x00\x12V\n\x17\x65lectrum_encryption_key\x18\x0e \x01(\x0b\x32\x33.shiftcrypto.bitbox02.ElectrumEncryptionKeyResponseH\x00\x12\x38\n\x07\x63\x61rdano\x18\x0f \x01(\x0b\x32%.shiftcrypto.bitbox02.CardanoResponseH\x00\x42\n\n\x08responseJ\x04\x08\x03\x10\x04\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'hww_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _ERROR._serialized_start=205
+  _ERROR._serialized_end=243
+  _SUCCESS._serialized_start=245
+  _SUCCESS._serialized_end=254
+  _REQUEST._serialized_start=257
+  _REQUEST._serialized_end=1993
+  _RESPONSE._serialized_start=1996
+  _RESPONSE._serialized_end=2901
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/hww_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/hww_pb2.pyi
new file mode 100644
index 000000000..2244acb8e
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/hww_pb2.pyi
@@ -0,0 +1,223 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+from . import backup_commands_pb2
+from . import bitbox02_system_pb2
+from . import btc_pb2
+import builtins
+from . import cardano_pb2
+from . import common_pb2
+from . import eth_pb2
+import google.protobuf.descriptor
+import google.protobuf.message
+from . import keystore_pb2
+from . import mnemonic_pb2
+from . import perform_attestation_pb2
+from . import system_pb2
+import typing
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class Error(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    CODE_FIELD_NUMBER: builtins.int
+    MESSAGE_FIELD_NUMBER: builtins.int
+    code: builtins.int
+    message: typing.Text
+    def __init__(self,
+        *,
+        code: builtins.int = ...,
+        message: typing.Text = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["code",b"code","message",b"message"]) -> None: ...
+global___Error = Error
+
+class Success(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    def __init__(self,
+        ) -> None: ...
+global___Success = Success
+
+class Request(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    DEVICE_NAME_FIELD_NUMBER: builtins.int
+    DEVICE_LANGUAGE_FIELD_NUMBER: builtins.int
+    DEVICE_INFO_FIELD_NUMBER: builtins.int
+    SET_PASSWORD_FIELD_NUMBER: builtins.int
+    CREATE_BACKUP_FIELD_NUMBER: builtins.int
+    SHOW_MNEMONIC_FIELD_NUMBER: builtins.int
+    BTC_PUB_FIELD_NUMBER: builtins.int
+    BTC_SIGN_INIT_FIELD_NUMBER: builtins.int
+    BTC_SIGN_INPUT_FIELD_NUMBER: builtins.int
+    BTC_SIGN_OUTPUT_FIELD_NUMBER: builtins.int
+    INSERT_REMOVE_SDCARD_FIELD_NUMBER: builtins.int
+    CHECK_SDCARD_FIELD_NUMBER: builtins.int
+    SET_MNEMONIC_PASSPHRASE_ENABLED_FIELD_NUMBER: builtins.int
+    LIST_BACKUPS_FIELD_NUMBER: builtins.int
+    RESTORE_BACKUP_FIELD_NUMBER: builtins.int
+    PERFORM_ATTESTATION_FIELD_NUMBER: builtins.int
+    REBOOT_FIELD_NUMBER: builtins.int
+    CHECK_BACKUP_FIELD_NUMBER: builtins.int
+    ETH_FIELD_NUMBER: builtins.int
+    RESET_FIELD_NUMBER: builtins.int
+    RESTORE_FROM_MNEMONIC_FIELD_NUMBER: builtins.int
+    FINGERPRINT_FIELD_NUMBER: builtins.int
+    BTC_FIELD_NUMBER: builtins.int
+    ELECTRUM_ENCRYPTION_KEY_FIELD_NUMBER: builtins.int
+    CARDANO_FIELD_NUMBER: builtins.int
+    @property
+    def device_name(self) -> bitbox02_system_pb2.SetDeviceNameRequest:
+        """removed: RandomNumberRequest random_number = 1;"""
+        pass
+    @property
+    def device_language(self) -> bitbox02_system_pb2.SetDeviceLanguageRequest: ...
+    @property
+    def device_info(self) -> bitbox02_system_pb2.DeviceInfoRequest: ...
+    @property
+    def set_password(self) -> bitbox02_system_pb2.SetPasswordRequest: ...
+    @property
+    def create_backup(self) -> backup_commands_pb2.CreateBackupRequest: ...
+    @property
+    def show_mnemonic(self) -> mnemonic_pb2.ShowMnemonicRequest: ...
+    @property
+    def btc_pub(self) -> btc_pb2.BTCPubRequest: ...
+    @property
+    def btc_sign_init(self) -> btc_pb2.BTCSignInitRequest: ...
+    @property
+    def btc_sign_input(self) -> btc_pb2.BTCSignInputRequest: ...
+    @property
+    def btc_sign_output(self) -> btc_pb2.BTCSignOutputRequest: ...
+    @property
+    def insert_remove_sdcard(self) -> bitbox02_system_pb2.InsertRemoveSDCardRequest: ...
+    @property
+    def check_sdcard(self) -> bitbox02_system_pb2.CheckSDCardRequest: ...
+    @property
+    def set_mnemonic_passphrase_enabled(self) -> mnemonic_pb2.SetMnemonicPassphraseEnabledRequest: ...
+    @property
+    def list_backups(self) -> backup_commands_pb2.ListBackupsRequest: ...
+    @property
+    def restore_backup(self) -> backup_commands_pb2.RestoreBackupRequest: ...
+    @property
+    def perform_attestation(self) -> perform_attestation_pb2.PerformAttestationRequest: ...
+    @property
+    def reboot(self) -> system_pb2.RebootRequest: ...
+    @property
+    def check_backup(self) -> backup_commands_pb2.CheckBackupRequest: ...
+    @property
+    def eth(self) -> eth_pb2.ETHRequest: ...
+    @property
+    def reset(self) -> bitbox02_system_pb2.ResetRequest: ...
+    @property
+    def restore_from_mnemonic(self) -> mnemonic_pb2.RestoreFromMnemonicRequest: ...
+    @property
+    def fingerprint(self) -> common_pb2.RootFingerprintRequest:
+        """removed: BitBoxBaseRequest bitboxbase = 23;"""
+        pass
+    @property
+    def btc(self) -> btc_pb2.BTCRequest: ...
+    @property
+    def electrum_encryption_key(self) -> keystore_pb2.ElectrumEncryptionKeyRequest: ...
+    @property
+    def cardano(self) -> cardano_pb2.CardanoRequest: ...
+    def __init__(self,
+        *,
+        device_name: typing.Optional[bitbox02_system_pb2.SetDeviceNameRequest] = ...,
+        device_language: typing.Optional[bitbox02_system_pb2.SetDeviceLanguageRequest] = ...,
+        device_info: typing.Optional[bitbox02_system_pb2.DeviceInfoRequest] = ...,
+        set_password: typing.Optional[bitbox02_system_pb2.SetPasswordRequest] = ...,
+        create_backup: typing.Optional[backup_commands_pb2.CreateBackupRequest] = ...,
+        show_mnemonic: typing.Optional[mnemonic_pb2.ShowMnemonicRequest] = ...,
+        btc_pub: typing.Optional[btc_pb2.BTCPubRequest] = ...,
+        btc_sign_init: typing.Optional[btc_pb2.BTCSignInitRequest] = ...,
+        btc_sign_input: typing.Optional[btc_pb2.BTCSignInputRequest] = ...,
+        btc_sign_output: typing.Optional[btc_pb2.BTCSignOutputRequest] = ...,
+        insert_remove_sdcard: typing.Optional[bitbox02_system_pb2.InsertRemoveSDCardRequest] = ...,
+        check_sdcard: typing.Optional[bitbox02_system_pb2.CheckSDCardRequest] = ...,
+        set_mnemonic_passphrase_enabled: typing.Optional[mnemonic_pb2.SetMnemonicPassphraseEnabledRequest] = ...,
+        list_backups: typing.Optional[backup_commands_pb2.ListBackupsRequest] = ...,
+        restore_backup: typing.Optional[backup_commands_pb2.RestoreBackupRequest] = ...,
+        perform_attestation: typing.Optional[perform_attestation_pb2.PerformAttestationRequest] = ...,
+        reboot: typing.Optional[system_pb2.RebootRequest] = ...,
+        check_backup: typing.Optional[backup_commands_pb2.CheckBackupRequest] = ...,
+        eth: typing.Optional[eth_pb2.ETHRequest] = ...,
+        reset: typing.Optional[bitbox02_system_pb2.ResetRequest] = ...,
+        restore_from_mnemonic: typing.Optional[mnemonic_pb2.RestoreFromMnemonicRequest] = ...,
+        fingerprint: typing.Optional[common_pb2.RootFingerprintRequest] = ...,
+        btc: typing.Optional[btc_pb2.BTCRequest] = ...,
+        electrum_encryption_key: typing.Optional[keystore_pb2.ElectrumEncryptionKeyRequest] = ...,
+        cardano: typing.Optional[cardano_pb2.CardanoRequest] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["btc",b"btc","btc_pub",b"btc_pub","btc_sign_init",b"btc_sign_init","btc_sign_input",b"btc_sign_input","btc_sign_output",b"btc_sign_output","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","create_backup",b"create_backup","device_info",b"device_info","device_language",b"device_language","device_name",b"device_name","electrum_encryption_key",b"electrum_encryption_key","eth",b"eth","fingerprint",b"fingerprint","insert_remove_sdcard",b"insert_remove_sdcard","list_backups",b"list_backups","perform_attestation",b"perform_attestation","reboot",b"reboot","request",b"request","reset",b"reset","restore_backup",b"restore_backup","restore_from_mnemonic",b"restore_from_mnemonic","set_mnemonic_passphrase_enabled",b"set_mnemonic_passphrase_enabled","set_password",b"set_password","show_mnemonic",b"show_mnemonic"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["btc",b"btc","btc_pub",b"btc_pub","btc_sign_init",b"btc_sign_init","btc_sign_input",b"btc_sign_input","btc_sign_output",b"btc_sign_output","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","create_backup",b"create_backup","device_info",b"device_info","device_language",b"device_language","device_name",b"device_name","electrum_encryption_key",b"electrum_encryption_key","eth",b"eth","fingerprint",b"fingerprint","insert_remove_sdcard",b"insert_remove_sdcard","list_backups",b"list_backups","perform_attestation",b"perform_attestation","reboot",b"reboot","request",b"request","reset",b"reset","restore_backup",b"restore_backup","restore_from_mnemonic",b"restore_from_mnemonic","set_mnemonic_passphrase_enabled",b"set_mnemonic_passphrase_enabled","set_password",b"set_password","show_mnemonic",b"show_mnemonic"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["request",b"request"]) -> typing.Optional[typing_extensions.Literal["device_name","device_language","device_info","set_password","create_backup","show_mnemonic","btc_pub","btc_sign_init","btc_sign_input","btc_sign_output","insert_remove_sdcard","check_sdcard","set_mnemonic_passphrase_enabled","list_backups","restore_backup","perform_attestation","reboot","check_backup","eth","reset","restore_from_mnemonic","fingerprint","btc","electrum_encryption_key","cardano"]]: ...
+global___Request = Request
+
+class Response(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    SUCCESS_FIELD_NUMBER: builtins.int
+    ERROR_FIELD_NUMBER: builtins.int
+    DEVICE_INFO_FIELD_NUMBER: builtins.int
+    PUB_FIELD_NUMBER: builtins.int
+    BTC_SIGN_NEXT_FIELD_NUMBER: builtins.int
+    LIST_BACKUPS_FIELD_NUMBER: builtins.int
+    CHECK_BACKUP_FIELD_NUMBER: builtins.int
+    PERFORM_ATTESTATION_FIELD_NUMBER: builtins.int
+    CHECK_SDCARD_FIELD_NUMBER: builtins.int
+    ETH_FIELD_NUMBER: builtins.int
+    FINGERPRINT_FIELD_NUMBER: builtins.int
+    BTC_FIELD_NUMBER: builtins.int
+    ELECTRUM_ENCRYPTION_KEY_FIELD_NUMBER: builtins.int
+    CARDANO_FIELD_NUMBER: builtins.int
+    @property
+    def success(self) -> global___Success: ...
+    @property
+    def error(self) -> global___Error: ...
+    @property
+    def device_info(self) -> bitbox02_system_pb2.DeviceInfoResponse:
+        """removed: RandomNumberResponse random_number = 3;"""
+        pass
+    @property
+    def pub(self) -> common_pb2.PubResponse: ...
+    @property
+    def btc_sign_next(self) -> btc_pb2.BTCSignNextResponse: ...
+    @property
+    def list_backups(self) -> backup_commands_pb2.ListBackupsResponse: ...
+    @property
+    def check_backup(self) -> backup_commands_pb2.CheckBackupResponse: ...
+    @property
+    def perform_attestation(self) -> perform_attestation_pb2.PerformAttestationResponse: ...
+    @property
+    def check_sdcard(self) -> bitbox02_system_pb2.CheckSDCardResponse: ...
+    @property
+    def eth(self) -> eth_pb2.ETHResponse: ...
+    @property
+    def fingerprint(self) -> common_pb2.RootFingerprintResponse: ...
+    @property
+    def btc(self) -> btc_pb2.BTCResponse: ...
+    @property
+    def electrum_encryption_key(self) -> keystore_pb2.ElectrumEncryptionKeyResponse: ...
+    @property
+    def cardano(self) -> cardano_pb2.CardanoResponse: ...
+    def __init__(self,
+        *,
+        success: typing.Optional[global___Success] = ...,
+        error: typing.Optional[global___Error] = ...,
+        device_info: typing.Optional[bitbox02_system_pb2.DeviceInfoResponse] = ...,
+        pub: typing.Optional[common_pb2.PubResponse] = ...,
+        btc_sign_next: typing.Optional[btc_pb2.BTCSignNextResponse] = ...,
+        list_backups: typing.Optional[backup_commands_pb2.ListBackupsResponse] = ...,
+        check_backup: typing.Optional[backup_commands_pb2.CheckBackupResponse] = ...,
+        perform_attestation: typing.Optional[perform_attestation_pb2.PerformAttestationResponse] = ...,
+        check_sdcard: typing.Optional[bitbox02_system_pb2.CheckSDCardResponse] = ...,
+        eth: typing.Optional[eth_pb2.ETHResponse] = ...,
+        fingerprint: typing.Optional[common_pb2.RootFingerprintResponse] = ...,
+        btc: typing.Optional[btc_pb2.BTCResponse] = ...,
+        electrum_encryption_key: typing.Optional[keystore_pb2.ElectrumEncryptionKeyResponse] = ...,
+        cardano: typing.Optional[cardano_pb2.CardanoResponse] = ...,
+        ) -> None: ...
+    def HasField(self, field_name: typing_extensions.Literal["btc",b"btc","btc_sign_next",b"btc_sign_next","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","device_info",b"device_info","electrum_encryption_key",b"electrum_encryption_key","error",b"error","eth",b"eth","fingerprint",b"fingerprint","list_backups",b"list_backups","perform_attestation",b"perform_attestation","pub",b"pub","response",b"response","success",b"success"]) -> builtins.bool: ...
+    def ClearField(self, field_name: typing_extensions.Literal["btc",b"btc","btc_sign_next",b"btc_sign_next","cardano",b"cardano","check_backup",b"check_backup","check_sdcard",b"check_sdcard","device_info",b"device_info","electrum_encryption_key",b"electrum_encryption_key","error",b"error","eth",b"eth","fingerprint",b"fingerprint","list_backups",b"list_backups","perform_attestation",b"perform_attestation","pub",b"pub","response",b"response","success",b"success"]) -> None: ...
+    def WhichOneof(self, oneof_group: typing_extensions.Literal["response",b"response"]) -> typing.Optional[typing_extensions.Literal["success","error","device_info","pub","btc_sign_next","list_backups","check_backup","perform_attestation","check_sdcard","eth","fingerprint","btc","electrum_encryption_key","cardano"]]: ...
+global___Response = Response
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/keystore_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/keystore_pb2.py
new file mode 100644
index 000000000..ec262caf7
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/keystore_pb2.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: keystore.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ekeystore.proto\x12\x14shiftcrypto.bitbox02\"/\n\x1c\x45lectrumEncryptionKeyRequest\x12\x0f\n\x07keypath\x18\x01 \x03(\r\",\n\x1d\x45lectrumEncryptionKeyResponse\x12\x0b\n\x03key\x18\x01 \x01(\tb\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'keystore_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _ELECTRUMENCRYPTIONKEYREQUEST._serialized_start=40
+  _ELECTRUMENCRYPTIONKEYREQUEST._serialized_end=87
+  _ELECTRUMENCRYPTIONKEYRESPONSE._serialized_start=89
+  _ELECTRUMENCRYPTIONKEYRESPONSE._serialized_end=133
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/keystore_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/keystore_pb2.pyi
new file mode 100644
index 000000000..3296d1162
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/keystore_pb2.pyi
@@ -0,0 +1,35 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+import builtins
+import google.protobuf.descriptor
+import google.protobuf.internal.containers
+import google.protobuf.message
+import typing
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class ElectrumEncryptionKeyRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    KEYPATH_FIELD_NUMBER: builtins.int
+    @property
+    def keypath(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.int]: ...
+    def __init__(self,
+        *,
+        keypath: typing.Optional[typing.Iterable[builtins.int]] = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["keypath",b"keypath"]) -> None: ...
+global___ElectrumEncryptionKeyRequest = ElectrumEncryptionKeyRequest
+
+class ElectrumEncryptionKeyResponse(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    KEY_FIELD_NUMBER: builtins.int
+    key: typing.Text
+    def __init__(self,
+        *,
+        key: typing.Text = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["key",b"key"]) -> None: ...
+global___ElectrumEncryptionKeyResponse = ElectrumEncryptionKeyResponse
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/mnemonic_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/mnemonic_pb2.py
new file mode 100644
index 000000000..8312f4d58
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/mnemonic_pb2.py
@@ -0,0 +1,29 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: mnemonic.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0emnemonic.proto\x12\x14shiftcrypto.bitbox02\"\x15\n\x13ShowMnemonicRequest\"H\n\x1aRestoreFromMnemonicRequest\x12\x11\n\ttimestamp\x18\x01 \x01(\r\x12\x17\n\x0ftimezone_offset\x18\x02 \x01(\x05\"6\n#SetMnemonicPassphraseEnabledRequest\x12\x0f\n\x07\x65nabled\x18\x01 \x01(\x08\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'mnemonic_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _SHOWMNEMONICREQUEST._serialized_start=40
+  _SHOWMNEMONICREQUEST._serialized_end=61
+  _RESTOREFROMMNEMONICREQUEST._serialized_start=63
+  _RESTOREFROMMNEMONICREQUEST._serialized_end=135
+  _SETMNEMONICPASSPHRASEENABLEDREQUEST._serialized_start=137
+  _SETMNEMONICPASSPHRASEENABLEDREQUEST._serialized_end=191
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/mnemonic_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/mnemonic_pb2.pyi
new file mode 100644
index 000000000..60dc3568c
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/mnemonic_pb2.pyi
@@ -0,0 +1,41 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+import builtins
+import google.protobuf.descriptor
+import google.protobuf.message
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class ShowMnemonicRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    def __init__(self,
+        ) -> None: ...
+global___ShowMnemonicRequest = ShowMnemonicRequest
+
+class RestoreFromMnemonicRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    TIMESTAMP_FIELD_NUMBER: builtins.int
+    TIMEZONE_OFFSET_FIELD_NUMBER: builtins.int
+    timestamp: builtins.int
+    timezone_offset: builtins.int
+    def __init__(self,
+        *,
+        timestamp: builtins.int = ...,
+        timezone_offset: builtins.int = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["timestamp",b"timestamp","timezone_offset",b"timezone_offset"]) -> None: ...
+global___RestoreFromMnemonicRequest = RestoreFromMnemonicRequest
+
+class SetMnemonicPassphraseEnabledRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    ENABLED_FIELD_NUMBER: builtins.int
+    enabled: builtins.bool
+    def __init__(self,
+        *,
+        enabled: builtins.bool = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["enabled",b"enabled"]) -> None: ...
+global___SetMnemonicPassphraseEnabledRequest = SetMnemonicPassphraseEnabledRequest
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/system_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/system_pb2.py
new file mode 100644
index 000000000..9f6e4bf2e
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/system_pb2.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: system.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0csystem.proto\x12\x14shiftcrypto.bitbox02\"s\n\rRebootRequest\x12<\n\x07purpose\x18\x01 \x01(\x0e\x32+.shiftcrypto.bitbox02.RebootRequest.Purpose\"$\n\x07Purpose\x12\x0b\n\x07UPGRADE\x10\x00\x12\x0c\n\x08SETTINGS\x10\x01\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'system_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _REBOOTREQUEST._serialized_start=38
+  _REBOOTREQUEST._serialized_end=153
+  _REBOOTREQUEST_PURPOSE._serialized_start=117
+  _REBOOTREQUEST_PURPOSE._serialized_end=153
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/system_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/system_pb2.pyi
new file mode 100644
index 000000000..a23b8693a
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/system_pb2.pyi
@@ -0,0 +1,36 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+import builtins
+import google.protobuf.descriptor
+import google.protobuf.internal.enum_type_wrapper
+import google.protobuf.message
+import typing
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class RebootRequest(google.protobuf.message.Message):
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    class _Purpose:
+        ValueType = typing.NewType('ValueType', builtins.int)
+        V: typing_extensions.TypeAlias = ValueType
+    class _PurposeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[RebootRequest._Purpose.ValueType], builtins.type):
+        DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
+        UPGRADE: RebootRequest._Purpose.ValueType  # 0
+        SETTINGS: RebootRequest._Purpose.ValueType  # 1
+    class Purpose(_Purpose, metaclass=_PurposeEnumTypeWrapper):
+        pass
+
+    UPGRADE: RebootRequest.Purpose.ValueType  # 0
+    SETTINGS: RebootRequest.Purpose.ValueType  # 1
+
+    PURPOSE_FIELD_NUMBER: builtins.int
+    purpose: global___RebootRequest.Purpose.ValueType
+    def __init__(self,
+        *,
+        purpose: global___RebootRequest.Purpose.ValueType = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["purpose",b"purpose"]) -> None: ...
+global___RebootRequest = RebootRequest
diff --git a/hwilib/devices/bitbox02_lib/communication/py.typed b/hwilib/devices/bitbox02_lib/communication/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/hwilib/devices/bitbox02_lib/communication/u2fhid/__init__.py b/hwilib/devices/bitbox02_lib/communication/u2fhid/__init__.py
new file mode 100644
index 000000000..306df7b73
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/u2fhid/__init__.py
@@ -0,0 +1,17 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Util functions to interact with u2fhid devices."""
+
+from .u2fhid import CID_BROADCAST, INIT, MSG, PING, U2FHid, WINK
diff --git a/hwilib/devices/bitbox02_lib/communication/u2fhid/u2fhid.py b/hwilib/devices/bitbox02_lib/communication/u2fhid/u2fhid.py
new file mode 100644
index 000000000..3e6e84db8
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/u2fhid/u2fhid.py
@@ -0,0 +1,161 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Implementations"""
+
+import struct
+import random
+from ..communication import PhysicalLayer, TransportLayer
+
+USB_REPORT_SIZE = 64
+
+ERR_NONE = 0x00
+ERR_INVALID_CMD = 0x01
+ERR_INVALID_PAR = 0x02
+ERR_INVALID_LEN = 0x03
+ERR_INVALID_SEQ = 0x04
+ERR_MSG_TIMEOUT = 0x05
+ERR_CHANNEL_BUSY = 0x06
+ERR_LOCK_REQUIRED = 0x0A
+ERR_INVALID_CID = 0x0B
+ERR_OTHER = 0x7F
+
+PING = 0x80 | 0x01
+MSG = 0x80 | 0x03
+LOCK = 0x80 | 0x04
+INIT = 0x80 | 0x06
+WINK = 0x80 | 0x08
+SYNC = 0x80 | 0x3C
+ERROR = 0x80 | 0x3F
+
+CID_BROADCAST = 0xFFFFFFFF
+
+
+class U2FHid(TransportLayer):
+    """U2F-over-HID transport layer."""
+
+    def __init__(self, device: PhysicalLayer):
+        self._device = device
+
+    def generate_cid(self) -> int:
+        """Generate a valid CID"""
+        # Exclude 0 and u32_max (0xffff_ffff)
+        return random.randrange(1, 0xFFFFFFFF)
+
+    # TODO: Create exceptions
+    def _throw_error(self, error_code: int) -> None:
+        if error_code == ERR_INVALID_CMD:
+            raise Exception("Received error: invalid command")
+        if error_code == ERR_INVALID_LEN:
+            raise Exception("Received error: invalid length")
+        if error_code == ERR_INVALID_SEQ:
+            raise Exception("Received error: invalid sequence")
+        if error_code == ERR_CHANNEL_BUSY:
+            raise Exception("Received error: channel busy")
+        if error_code == 0x7E:
+            raise Exception("Received error: encryption failed")
+        if error_code == ERR_OTHER:
+            raise Exception("Received unknown error")
+        raise Exception("Received error: %d" % error_code)
+
+    def write(self, data: bytes, endpoint: int, cid: int) -> None:
+        """
+        Send data to the device.
+
+        Args:
+            data: Data to send
+            endpoint: U2F HID command (endpoint selection)
+            cid: U2F HID channel ID
+        Throws:
+            ValueError: In case any value is out of range
+        """
+        if endpoint < 0 or endpoint > 0xFF:
+            raise ValueError("Channel command (endpoint) is out of range '0 < endpoint <= 0xFF'")
+        if cid < 0 or cid > 0xFFFFFFFF:
+            raise ValueError("Channel id is out of range '0 < cid <= 0xFFFFFFFF'")
+        data = bytearray(data)
+        data_len = len(data)
+        if data_len > 0xFFFF:
+            raise ValueError("Data is too large 'size <= 0xFFFF'")
+        seq = 0
+        idx = 0
+        buf = b""
+        # Allow to write an empty packet
+        single_empty_write = data_len == 0
+        while idx < data_len or single_empty_write:
+            if idx == 0:
+                # INIT frame
+                buf = data[idx : idx + min(data_len, USB_REPORT_SIZE - 7)]
+                self._device.write(
+                    b"\0"
+                    + struct.pack(">IBH", cid, endpoint, data_len)
+                    + buf
+                    + b"\xEE" * (USB_REPORT_SIZE - 7 - len(buf))
+                )
+            else:
+                # CONT frame
+                buf = data[idx : idx + min(data_len, USB_REPORT_SIZE - 5)]
+                self._device.write(
+                    b"\0"
+                    + struct.pack(">IB", cid, seq)
+                    + buf
+                    + b"\xEE" * (USB_REPORT_SIZE - 5 - len(buf))
+                )
+                seq += 1
+            idx += len(buf)
+            single_empty_write = False
+
+    def read(self, endpoint: int, cid: int) -> bytes:
+        """
+        Receive data from the device.
+
+        Args:
+            endpoint: The expected returned U2F HID command
+            cid: The expected returned U2F HID channel ID
+        Returns:
+            The read message combined from the u2fhid packets
+        Throws:
+            ValueError: In case any value is out of range
+            Exception: In case of USB communication issues
+        """
+        if endpoint < 0 or endpoint > 0xFF:
+            raise ValueError("Endpoint/U2F command is out of range '0 < endpoint <= 0xFF'")
+        if cid < 0 or cid > 0xFFFFFFFF:
+            raise ValueError("Channel id is out of range '0 < cid <= 0xFFFFFFFF'")
+        timeout_ms = 5000000
+        buf = self._device.read(USB_REPORT_SIZE, timeout_ms)
+        if len(buf) >= 3:
+            reply_cid = ((buf[0] * 256 + buf[1]) * 256 + buf[2]) * 256 + buf[3]
+            reply_cmd = buf[4]
+            data_len = buf[5] * 256 + buf[6]
+            data = buf[7:]
+            idx = len(buf) - 7
+            if reply_cmd == ERROR:
+                self._throw_error(data[0])
+            while idx < data_len:
+                # CONT response
+                buf = self._device.read(USB_REPORT_SIZE, timeout_ms)
+                if len(buf) < 3:
+                    raise Exception("Did not receive a continuation frame after 5000 seconds.")
+                data += buf[5:]
+                idx += len(buf) - 5
+            if reply_cid != cid:
+                raise Exception(f"- USB channel ID mismatch {reply_cid:x} != {cid:x}")
+            if reply_cmd != endpoint:
+                raise Exception(f"- USB command mismatch {reply_cmd:x} != {endpoint:x}")
+            return bytes(data[:data_len])
+        raise Exception("Did not read anything after 5000 seconds.")
+
+    def close(self) -> None:
+        self._device.close()
diff --git a/hwilib/devices/bitbox02_lib/py.typed b/hwilib/devices/bitbox02_lib/py.typed
new file mode 100644
index 000000000..e69de29bb
diff --git a/hwilib/devices/bitbox02_lib/util.py b/hwilib/devices/bitbox02_lib/util.py
new file mode 100644
index 000000000..8b98c91a1
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/util.py
@@ -0,0 +1,218 @@
+# Copyright 2019 Shift Cryptosecurity AG
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Useful functions"""
+
+from typing import Any, Dict, Optional, List, Tuple
+from pathlib import Path
+import json
+import os
+import binascii
+import base64
+import platform
+
+from ..._base58 import decode_check
+from noise.backends.default.keypairs import KeyPair25519
+
+from .bitbox02 import common
+from .communication import bitbox_api_protocol
+
+
+def parse_xpub(xpub: str) -> common.XPub:
+    """
+    Parse an xpub to a protobuf XPub.
+    The version is stripped, so the xpub can be any format (xpub, ypub, etc.).
+    """
+
+    decoded = decode_check(xpub)
+    decoded = decoded[4:]
+    depth, decoded = decoded[:1], decoded[1:]
+    parent_fp, decoded = decoded[:4], decoded[4:]
+    child_num, decoded = decoded[:4], decoded[4:]
+    chain_code, decoded = decoded[:32], decoded[32:]
+    pubkey, decoded = decoded[:33], decoded[33:]
+    assert len(decoded) == 0
+    return common.XPub(
+        depth=depth,
+        parent_fingerprint=parent_fp,
+        child_num=int.from_bytes(child_num, "big"),
+        chain_code=chain_code,
+        public_key=pubkey,
+    )
+
+
+class UserCache:
+    """Data structure to hold keys"""
+
+    def __init__(self, raw_cache: Optional[str] = None):
+        if raw_cache is None:
+            self.app_static_privkey = None
+            self.device_static_pubkeys: List[bytes] = []
+            return
+        (privkey, pubkeys) = UserCache.deserialize(raw_cache)
+        self.app_static_privkey = privkey
+        self.device_static_pubkeys = pubkeys
+
+    def serialize(self) -> str:
+        """Serialize struct to string"""
+        pubkeys = [binascii.hexlify(x).decode("utf-8") for x in self.device_static_pubkeys]
+        privkey = None
+        if self.app_static_privkey is not None:
+            privkey = binascii.hexlify(self.app_static_privkey).decode("utf-8")
+        return json.dumps({"device_static_pubkeys": pubkeys, "app_static_privkey": privkey})
+
+    @staticmethod
+    def deserialize(raw: str) -> Tuple[Optional[bytes], List[bytes]]:
+        """Deserialize content from disk to struct"""
+        try:
+            data = json.loads(raw)
+            privkey = None
+            if data["app_static_privkey"] is not None:
+                privkey = binascii.unhexlify(data["app_static_privkey"])
+            pubkeys = [binascii.unhexlify(x) for x in data["device_static_pubkeys"]]
+            return (privkey, pubkeys)
+        except json.JSONDecodeError:
+            return (None, [])
+        except KeyError:
+            return (None, [])
+
+
+class NoiseConfigUserCache(bitbox_api_protocol.BitBoxNoiseConfig):
+    """
+    A noise config that stores the keys in a file in XDG_CACHE_HOME or ~/.cache.
+    Currently intended as a developer help only (currently no macOS/Windows support).
+    """
+
+    def __init__(self, appid: str) -> None:
+        """
+        Args:
+            appid: A string that uniqely identifies your application. It will be used as the name
+            of the cache directory. Directory separators will create subdirectories, e.g.
+            "shift/test1".
+        """
+        self._cache_file_path = NoiseConfigUserCache._find_cache_file(appid)
+        super().__init__()
+
+    @staticmethod
+    def _find_cache_file(appid: str) -> Path:
+        cachedir_env = os.environ.get("XDG_CACHE_HOME", "")
+        if cachedir_env == "":
+            homedir = os.environ.get("HOME", "")
+            if homedir == "":
+                raise RuntimeError("Can't find cache dir")
+            cachedir = Path(homedir) / ".cache"
+        else:
+            cachedir = Path(cachedir_env)
+        return cachedir / appid / "bitbox02.dat"
+
+    def _read_cache(self) -> UserCache:
+        try:
+            with self._cache_file_path.open("r") as fileh:
+                return UserCache(fileh.read())
+        except FileNotFoundError:
+            return UserCache()
+
+    def _write_cache(self, data: UserCache) -> None:
+        self._cache_file_path.parent.mkdir(parents=True, exist_ok=True)
+        with self._cache_file_path.open("w") as fileh:
+            fileh.write(data.serialize())
+
+    def contains_device_static_pubkey(self, pubkey: bytes) -> bool:
+        data = self._read_cache()
+        if pubkey in data.device_static_pubkeys:
+            return True
+        return False
+
+    def add_device_static_pubkey(self, pubkey: bytes) -> None:
+        if not self.contains_device_static_pubkey(pubkey):
+            data = self._read_cache()
+            data.device_static_pubkeys.append(pubkey)
+            self._write_cache(data)
+
+    def get_app_static_privkey(self) -> Optional[bytes]:
+        data = self._read_cache()
+        return data.app_static_privkey
+
+    def set_app_static_privkey(self, privkey: bytes) -> None:
+        data = self._read_cache()
+        data.app_static_privkey = privkey
+        self._write_cache(data)
+
+
+class BitBoxAppNoiseConfig(bitbox_api_protocol.BitBoxNoiseConfig):
+    """
+    Noise config that reads and stores the noise keys in the same location as the BitBoxApp.
+    This allows a third party BitBox02 integration to re-use the pairing with the BitBoxApp.
+    """
+
+    _DEVICE_NOISE_STATIC_PUBKEYS = "deviceNoiseStaticPubkeys"
+    _APP_NOISE_STATIC_KEYPAIR = "appNoiseStaticKeypair"
+
+    def __init__(self) -> None:
+        system = platform.system()
+        if system == "Linux":
+            folder = Path(
+                os.environ.get(
+                    "XDG_CONFIG_HOME",
+                    # fallback
+                    os.path.join(os.environ["HOME"], ".config"),
+                )
+            )
+        elif system == "Darwin":
+            folder = Path(os.environ["HOME"]) / "Library" / "Application Support"
+        elif system == "Windows":
+            folder = Path(os.environ["APPDATA"])
+        else:
+            raise NotImplementedError("Unknown system: {}".format(system))
+        self._filename = folder / "bitbox" / "bitbox02" / "bitbox02.json"
+
+    def _read(self) -> Any:
+        try:
+            with self._filename.open("r") as fileh:
+                return json.load(fileh)
+        except FileNotFoundError:
+            return {}
+        except json.JSONDecodeError:
+            return {}
+
+    def _write(self, data: Dict[str, object]) -> None:
+        self._filename.parent.mkdir(parents=True, exist_ok=True)
+        with self._filename.open("w") as fileh:
+            json.dump(data, fileh)
+
+    def contains_device_static_pubkey(self, pubkey: bytes) -> bool:
+        data = self._read()
+        return base64.b64encode(pubkey).decode() in data.get(self._DEVICE_NOISE_STATIC_PUBKEYS, [])
+
+    def add_device_static_pubkey(self, pubkey: bytes) -> None:
+        if not self.contains_device_static_pubkey(pubkey):
+            data = self._read()
+            data.setdefault(self._DEVICE_NOISE_STATIC_PUBKEYS, [])
+            data[self._DEVICE_NOISE_STATIC_PUBKEYS].append(base64.b64encode(pubkey).decode())
+            self._write(data)
+
+    def get_app_static_privkey(self) -> Optional[bytes]:
+        data = self._read()
+        if self._APP_NOISE_STATIC_KEYPAIR not in data:
+            return None
+
+        return base64.b64decode(data[self._APP_NOISE_STATIC_KEYPAIR]["private"])
+
+    def set_app_static_privkey(self, privkey: bytes) -> None:
+        data = self._read()
+        pubkey = KeyPair25519.from_private_bytes(privkey).public_bytes
+        data[self._APP_NOISE_STATIC_KEYPAIR] = {
+            "private": base64.b64encode(privkey).decode(),
+            "public": base64.b64encode(pubkey).decode(),
+        }
+        self._write(data)
diff --git a/poetry.lock b/poetry.lock
index 7dc238071..2316be711 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,10 +1,9 @@
-# This file is automatically @generated by Poetry and should not be changed by hand.
+# This file is automatically @generated by Poetry 1.5.1 and should not be changed by hand.
 
 [[package]]
 name = "alabaster"
 version = "0.7.13"
 description = "A configurable sidebar-enabled Sphinx theme"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -16,7 +15,6 @@ files = [
 name = "altgraph"
 version = "0.17.3"
 description = "Python graph (network) package"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -28,7 +26,6 @@ files = [
 name = "autopep8"
 version = "1.5.7"
 description = "A tool that automatically formats Python code to conform to the PEP 8 style guide"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -44,7 +41,6 @@ toml = "*"
 name = "babel"
 version = "2.11.0"
 description = "Internationalization utilities"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -55,47 +51,10 @@ files = [
 [package.dependencies]
 pytz = ">=2015.7"
 
-[[package]]
-name = "base58"
-version = "2.1.1"
-description = "Base58 and Base58Check implementation."
-category = "main"
-optional = false
-python-versions = ">=3.5"
-files = [
-    {file = "base58-2.1.1-py3-none-any.whl", hash = "sha256:11a36f4d3ce51dfc1043f3218591ac4eb1ceb172919cebe05b52a5bcc8d245c2"},
-    {file = "base58-2.1.1.tar.gz", hash = "sha256:c5d0cb3f5b6e81e8e35da5754388ddcc6d0d14b6c6a132cb93d69ed580a7278c"},
-]
-
-[package.extras]
-tests = ["PyHamcrest (>=2.0.2)", "mypy", "pytest (>=4.6)", "pytest-benchmark", "pytest-cov", "pytest-flake8"]
-
-[[package]]
-name = "bitbox02"
-version = "6.1.1"
-description = "Python library for bitbox02 communication"
-category = "main"
-optional = false
-python-versions = ">=3.6"
-files = [
-    {file = "bitbox02-6.1.1-py3-none-any.whl", hash = "sha256:f37f1e571f06aa0a4005441ca1948b53e68e3bef0b642963303b42810e2b1486"},
-    {file = "bitbox02-6.1.1.tar.gz", hash = "sha256:99503409d6c61899f8e11eb11e7a29866b2754cb02c03acf34cdef99755aedd8"},
-]
-
-[package.dependencies]
-base58 = ">=2.0.0"
-ecdsa = ">=0.14"
-hidapi = ">=0.7.99.post21"
-noiseprotocol = ">=0.3"
-protobuf = ">=3.20"
-semver = ">=2.8.1"
-typing-extensions = ">=3.7.4"
-
 [[package]]
 name = "cbor"
 version = "1.0.0"
 description = "RFC 7049 - Concise Binary Object Representation"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -106,7 +65,6 @@ files = [
 name = "certifi"
 version = "2022.12.7"
 description = "Python package for providing Mozilla's CA Bundle."
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -118,7 +76,6 @@ files = [
 name = "cffi"
 version = "1.15.1"
 description = "Foreign Function Interface for Python calling C code."
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -195,7 +152,6 @@ pycparser = "*"
 name = "charset-normalizer"
 version = "3.0.1"
 description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -293,7 +249,6 @@ files = [
 name = "colorama"
 version = "0.4.6"
 description = "Cross-platform colored terminal text."
-category = "dev"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
@@ -305,7 +260,6 @@ files = [
 name = "cryptography"
 version = "39.0.1"
 description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
-category = "main"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -351,7 +305,6 @@ tox = ["tox"]
 name = "docutils"
 version = "0.18.1"
 description = "Docutils -- Python Documentation Utilities"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
 files = [
@@ -363,7 +316,6 @@ files = [
 name = "ecdsa"
 version = "0.18.0"
 description = "ECDSA cryptographic signature library (pure python)"
-category = "main"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -382,7 +334,6 @@ gmpy2 = ["gmpy2"]
 name = "flake8"
 version = "3.9.2"
 description = "the modular source code checker: pep8 pyflakes and co"
-category = "dev"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
 files = [
@@ -398,53 +349,52 @@ pyflakes = ">=2.3.0,<2.4.0"
 
 [[package]]
 name = "hidapi"
-version = "0.13.1"
+version = "0.14.0"
 description = "A Cython interface to the hidapi from https://github.com/libusb/hidapi"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
-    {file = "hidapi-0.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e245b5177fc59548e4f76400a07a7882fca48a1c650f7c5d902afe519869bd0"},
-    {file = "hidapi-0.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ec97dcb6697659be0878aac14143ba00ab43e86a6bbec2fa6af46f833ab69743"},
-    {file = "hidapi-0.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a48a0c3f1113ac05d389ae4eff00485a253c63dc6e7c7c17ae3026d063ca809a"},
-    {file = "hidapi-0.13.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b2421ace55d1cca96b8d5c09f918e7662b6ac7b7dbb0f624814e831dab5062e"},
-    {file = "hidapi-0.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:571e3a07bcbdc13c2d2af9bd09839e6f45cfbac8d5e2d28b7e1b844427813162"},
-    {file = "hidapi-0.13.1-cp310-cp310-win32.whl", hash = "sha256:6c0a285b3720d1277f318b574d987483d6127b75d7d76decdd6c85c724af8ef7"},
-    {file = "hidapi-0.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:da551cdf88122e9d7dbb93557efa69d35fb1b1fea0510a5bd461af331ad92892"},
-    {file = "hidapi-0.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6490021f7d8227fb6277355f143539404c1b9074d77d64febbe0d4a629f0835b"},
-    {file = "hidapi-0.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ebffcf48798bbf2018a86f1465eeabce0da98c2066c99a121db97dd397b798b3"},
-    {file = "hidapi-0.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c6ae8a0b69ca9ae0ad3f948e1174231a9b781a70ff3642e6b110dfec770cf59"},
-    {file = "hidapi-0.13.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:93b95d8bb6c87f6db03b6eb9faffb2fe977ac788791898651826c1b61a4734c1"},
-    {file = "hidapi-0.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aca9899e7fccc6f9e6a909681c5d97cc2ba224683d886c96bcdca3165454d875"},
-    {file = "hidapi-0.13.1-cp311-cp311-win32.whl", hash = "sha256:93dfc2739e0d3f773ae148fed9c0f020c13e17d9d512cf73b54a5e60f0b28c68"},
-    {file = "hidapi-0.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:68b9281c22e7832e1d140d3eb65718486b61ac24701837c0ff211f2195bedfb8"},
-    {file = "hidapi-0.13.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:3c190d4f8be403c8ee543684013e85494ac690da2d636a03a4f7f5b75b41bba5"},
-    {file = "hidapi-0.13.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cb651134546cabc1e515b38626732770ec3ab0157b33dc132fc2f4607858658"},
-    {file = "hidapi-0.13.1-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ab4dd2e19918beff50638be89b9626191df4514456fd7388611762cdbbd46cc"},
-    {file = "hidapi-0.13.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871808f5b4c8049f758b94375e24bfe87c8a58f91f7c2ca4c482ab003c9af9a7"},
-    {file = "hidapi-0.13.1-cp36-cp36m-win32.whl", hash = "sha256:5a51521200fecd677695d7bd12a6efdccf315a16929a65b836fb7ec8596a24f4"},
-    {file = "hidapi-0.13.1-cp36-cp36m-win_amd64.whl", hash = "sha256:d1e91ef0cca8185a858491d9e253f585e58888c5350013ab9305caf4b7f563ba"},
-    {file = "hidapi-0.13.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:8a735ea49c68d6008b4e07b3de5b4c211f875c262dd14d6669a3629bc482a375"},
-    {file = "hidapi-0.13.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8647eeb286d1b46104a297258b4af15e7878b3dbdb9951ba01cf233b2ff513a"},
-    {file = "hidapi-0.13.1-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf6a90f988c3a1b1910f441c29e4c8c21150c6cb0ed5a8be3bea715aed02002d"},
-    {file = "hidapi-0.13.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58580360dc69206a54efccc6d234baa6dbfa56394386ff77bdd67228b27c9759"},
-    {file = "hidapi-0.13.1-cp37-cp37m-win32.whl", hash = "sha256:1eb1247930ebf45275937db011856880dc6e3c961290dc8783835230312250b6"},
-    {file = "hidapi-0.13.1-cp37-cp37m-win_amd64.whl", hash = "sha256:155bce0c0741ee3b4cf93ac53d02526ef16008a1ef3d9071cd67dd4e190c02d8"},
-    {file = "hidapi-0.13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2f013d7e107aef90a4c79802f7e44f5280f9fd62d3d43e307dd7d108c95277cd"},
-    {file = "hidapi-0.13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:afc7991c12235f3392b5f744b75878429dbf5a3a4a0d51ca18b1e82ebcce7033"},
-    {file = "hidapi-0.13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3fcfe6e4a75651baf704001ddf765326ddd64b55b1ba6cd0e2e0851409c27c4"},
-    {file = "hidapi-0.13.1-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:64ec0dcce28232354a91d4575a74eab77fcbb4379fa2f4650e9eaadd7561d887"},
-    {file = "hidapi-0.13.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8174e095386a37eb3d8eb08e942f3aa7e6bd703c296dbb6c45cbca40862a7253"},
-    {file = "hidapi-0.13.1-cp38-cp38-win32.whl", hash = "sha256:2dcaf4b9df91a10c0e2904f175c0bfed53d51a3fe73d2aba7061ddc360249f86"},
-    {file = "hidapi-0.13.1-cp38-cp38-win_amd64.whl", hash = "sha256:f5a16a8a97ac944775369713b35bb47a7cb6b3091458e7cfe4fdec3f7ac9c29e"},
-    {file = "hidapi-0.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0933fef6e1599c38093bec22a3dded359fb0a9ce57ab43db8947eca7308fe0a8"},
-    {file = "hidapi-0.13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:20c06a2256696e83c5f2789dcd9a8109009af5f3e75af0082934867fc6de5283"},
-    {file = "hidapi-0.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b59778cec7ef9f6151778bf65a1b1dd164014238d7d4345c8e376c4a4baac0e"},
-    {file = "hidapi-0.13.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22d5af6f776fffce8078d6d3785dc8c1647688183c1dbda24acf6dd841d76ac3"},
-    {file = "hidapi-0.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ed536627429bc299b28188a28d2fe4230b472d737ee9dc5c8376fd66c418674"},
-    {file = "hidapi-0.13.1-cp39-cp39-win32.whl", hash = "sha256:63494ccce46bf60491d7e44f1371443a178e21afa287b341288057bfbefbe872"},
-    {file = "hidapi-0.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:5696e1479c203e6123db5f6c5effe6fdca3a5da332bd9ba559687056af7b1d3c"},
-    {file = "hidapi-0.13.1.tar.gz", hash = "sha256:99b18b28ec414ef9b604ddaed08182e486a400486f31ca56f61d537eed1d17cf"},
+    {file = "hidapi-0.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f68bbf88805553911e7e5a9b91136c96a54042b6e3d82d39d733d2edb46ff9a6"},
+    {file = "hidapi-0.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b264c6a1a1a0cacacc82299785415bec91184cb3e4a77d127c40016086705327"},
+    {file = "hidapi-0.14.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01929fbbe206ebcb0bad9b8e925e16de0aa8f872bf80a263f599e519866d9900"},
+    {file = "hidapi-0.14.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1b4052f17321f5f0b641e020eae87db5bb0103f893198e61b2495358db83ddab"},
+    {file = "hidapi-0.14.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:349976417f7f3371c7133a6427ed8f4faa06fbd93e9b5309d86689f25f191150"},
+    {file = "hidapi-0.14.0-cp310-cp310-win32.whl", hash = "sha256:7ef0f40a02e0b56fe2e7c93dfc9810245f2feeaa0c2ea76654d0768722883639"},
+    {file = "hidapi-0.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fdc08eb19f2fffb989124d1dbea3aa62dd0036615bbf464ceafee0353673bf4"},
+    {file = "hidapi-0.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4046bbfc67c5587ca638b875858569a8787e6955eff5dea4e424044de09fe7e4"},
+    {file = "hidapi-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:15f1fd34b0719d1e4d1bbc0bce325b318ee3e85c36fac0d23c6fb9d7f4d611db"},
+    {file = "hidapi-0.14.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c0959d89bc95acb4f9e6d58c8562281e22694959e42c10108193a1362b4fcd9"},
+    {file = "hidapi-0.14.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1b1ded4a823cc5c2075a622b48d02bc0a72f57579ea24c956ef29649a49eb66"},
+    {file = "hidapi-0.14.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2906ad143ec40009c33348ab4b3f7a9bdaa87b65bdc55983399bed47ee90a818"},
+    {file = "hidapi-0.14.0-cp311-cp311-win32.whl", hash = "sha256:651c2382e974e866d78334cdde3c290a04fcbab4cec940c0d3586d77d11b9566"},
+    {file = "hidapi-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:de293e7291b1ec813a97e42625c2c0a41b0d25d495b3dc5864bbb3dbbb5a719d"},
+    {file = "hidapi-0.14.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:1370bc6a364fd292accd580a8d7bac4219932144d149f3a513bb472581eac421"},
+    {file = "hidapi-0.14.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6ef0bdc69310cfdff83faf96c75492ac3d8cf355af275904f1dd90a3c5f24a4"},
+    {file = "hidapi-0.14.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e245719a5ede83c779dd99a4553002ae684d92d0f3e4274dcf06882b063f127"},
+    {file = "hidapi-0.14.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:833a32c3e44780f37d46dffd559b8e245034c92ae25060f752e4f34e9c7efe24"},
+    {file = "hidapi-0.14.0-cp36-cp36m-win32.whl", hash = "sha256:365d7c9fdcae71ae41797dc2dd062dfed4362d1b36d21fa62afbc16c5ec3cd5a"},
+    {file = "hidapi-0.14.0-cp36-cp36m-win_amd64.whl", hash = "sha256:810ad22831e4a150c2d6f27141fcf2826fd085ccacf4262d5c742c90aa81cd54"},
+    {file = "hidapi-0.14.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3ed9f993a6f8a611c11ef213968c6972e17d7e8b27936349884c475dc0309e71"},
+    {file = "hidapi-0.14.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fb47a0a8c3a6797306ea9eb8d1bdad68e5493ef5c8fa2e644501d56f2677551"},
+    {file = "hidapi-0.14.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b4513311fad7e499ebb0d7a26178557b85044983199a280cb95c2038902fe1a0"},
+    {file = "hidapi-0.14.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dff930adb37d1bcaeca3cf0dcec00eb72c109aa42c84858809cbae2972d79661"},
+    {file = "hidapi-0.14.0-cp37-cp37m-win32.whl", hash = "sha256:60c034ec3ef3e5679232d9e6c003c4848e4772032d683f0b91ddb84b87d8698d"},
+    {file = "hidapi-0.14.0-cp37-cp37m-win_amd64.whl", hash = "sha256:c8bba64d6ed49fa7ea4f4515986450223f5c744be448c846fb0614bc53b536bd"},
+    {file = "hidapi-0.14.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:48e2cf77626f3cfdda9624de3be7f9c55e37efbb39882d2e96a92d38893a09cb"},
+    {file = "hidapi-0.14.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a6edc57962a9f30bff73fc0cc80915c9da9ab3e0892c601263198f8d21d8dfff"},
+    {file = "hidapi-0.14.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e822e899c13eb1e3a575712d7be5bd03a9103f6027b00ab4351c8404cec5719d"},
+    {file = "hidapi-0.14.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bb87cf8f23c15346bc1487e6f39d11b37d3ff7788037d3760b7907ea325b6d2c"},
+    {file = "hidapi-0.14.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93697007df8ba38ab3ae3e777a6875cd1775fc720afe27e4c624cecbab7720de"},
+    {file = "hidapi-0.14.0-cp38-cp38-win32.whl", hash = "sha256:b054abf40b5aa7122314af59d0244fa274a50c4276d20695d8b7ff69564beb95"},
+    {file = "hidapi-0.14.0-cp38-cp38-win_amd64.whl", hash = "sha256:f575381efa788e1a894c68439644817b152b8a68ead643e42c23ba28eeedc33b"},
+    {file = "hidapi-0.14.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5299d74d96bdc9eaa83496c972048db0027d012a08440b33bdb6dd10a7491da9"},
+    {file = "hidapi-0.14.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c78ff5c46128bdf68b2c4e4b08fac7765ef79f6ee7e17c8a2f7d3090a591d97"},
+    {file = "hidapi-0.14.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e635c037d28e1ceded2043d81b879d81348a278d1ae668954a5a7a7d383f7d7"},
+    {file = "hidapi-0.14.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1425f523258d25d8f32a6493978532477c4d7507f5f9252417b1d629427871e"},
+    {file = "hidapi-0.14.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96ecea60915212e59940db41c2a91709ebd4ec6a04e03b0db37a4ddb6825bee6"},
+    {file = "hidapi-0.14.0-cp39-cp39-win32.whl", hash = "sha256:b4a0feac62d80eca36e2c8035fe4f57c440fbfcd9273a909112cb5bd9baae449"},
+    {file = "hidapi-0.14.0-cp39-cp39-win_amd64.whl", hash = "sha256:ed112c9ba0adf41d7e04bf5389dc150ada4d94a6ef1cb56c325d5aed1e4e07d2"},
+    {file = "hidapi-0.14.0.tar.gz", hash = "sha256:a7cb029286ced5426a381286526d9501846409701a29c2538615c3d1a612b8be"},
 ]
 
 [package.dependencies]
@@ -454,7 +404,6 @@ setuptools = ">=19.0"
 name = "idna"
 version = "3.4"
 description = "Internationalized Domain Names in Applications (IDNA)"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -466,7 +415,6 @@ files = [
 name = "imagesize"
 version = "1.4.1"
 description = "Getting image size from png/jpeg/jpeg2000/gif file"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -478,7 +426,6 @@ files = [
 name = "importlib-metadata"
 version = "6.0.0"
 description = "Read metadata from Python packages"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -499,7 +446,6 @@ testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packag
 name = "jinja2"
 version = "3.1.2"
 description = "A very fast and expressive template engine."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -517,7 +463,6 @@ i18n = ["Babel (>=2.7)"]
 name = "libusb1"
 version = "2.0.1"
 description = "Pure-python wrapper for libusb-1.0"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -531,7 +476,6 @@ files = [
 name = "macholib"
 version = "1.16.2"
 description = "Mach-O header analysis and editing"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -546,7 +490,6 @@ altgraph = ">=0.17"
 name = "markupsafe"
 version = "2.1.2"
 description = "Safely add untrusted strings to HTML/XML markup."
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -606,7 +549,6 @@ files = [
 name = "mccabe"
 version = "0.6.1"
 description = "McCabe checker, plugin for flake8"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -618,7 +560,6 @@ files = [
 name = "mnemonic"
 version = "0.20"
 description = "Implementation of Bitcoin BIP-0039"
-category = "main"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -630,7 +571,6 @@ files = [
 name = "mypy"
 version = "0.991"
 description = "Optional static typing for Python"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -682,7 +622,6 @@ reports = ["lxml"]
 name = "mypy-extensions"
 version = "1.0.0"
 description = "Type system extensions for programs checked with the mypy type checker."
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -694,7 +633,6 @@ files = [
 name = "noiseprotocol"
 version = "0.3.1"
 description = "Implementation of Noise Protocol Framework"
-category = "main"
 optional = false
 python-versions = "~=3.5"
 files = [
@@ -709,7 +647,6 @@ cryptography = ">=2.8"
 name = "packaging"
 version = "23.0"
 description = "Core utilities for Python packages"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -721,7 +658,6 @@ files = [
 name = "pefile"
 version = "2023.2.7"
 description = "Python PE parsing module"
-category = "dev"
 optional = false
 python-versions = ">=3.6.0"
 files = [
@@ -731,32 +667,30 @@ files = [
 
 [[package]]
 name = "protobuf"
-version = "4.22.0"
+version = "4.23.3"
 description = ""
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "protobuf-4.22.0-cp310-abi3-win32.whl", hash = "sha256:b2fea9dc8e3c0f32c38124790ef16cba2ee0628fe2022a52e435e1117bfef9b1"},
-    {file = "protobuf-4.22.0-cp310-abi3-win_amd64.whl", hash = "sha256:a33a273d21852f911b8bda47f39f4383fe7c061eb1814db2c76c9875c89c2491"},
-    {file = "protobuf-4.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:e894e9ae603e963f0842498c4cd5d39c6a60f0d7e4c103df50ee939564298658"},
-    {file = "protobuf-4.22.0-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:7c535d126e7dcc714105ab20b418c4fedbd28f8b8afc42b7350b1e317bbbcc71"},
-    {file = "protobuf-4.22.0-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:86c3d20428b007537ba6792b475c0853bba7f66b1f60e610d913b77d94b486e4"},
-    {file = "protobuf-4.22.0-cp37-cp37m-win32.whl", hash = "sha256:1669cb7524221a8e2d9008d0842453dbefdd0fcdd64d67672f657244867635fb"},
-    {file = "protobuf-4.22.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ab4d043865dd04e6b09386981fe8f80b39a1e46139fb4a3c206229d6b9f36ff6"},
-    {file = "protobuf-4.22.0-cp38-cp38-win32.whl", hash = "sha256:29288813aacaa302afa2381db1d6e0482165737b0afdf2811df5fa99185c457b"},
-    {file = "protobuf-4.22.0-cp38-cp38-win_amd64.whl", hash = "sha256:e474b63bab0a2ea32a7b26a4d8eec59e33e709321e5e16fb66e766b61b82a95e"},
-    {file = "protobuf-4.22.0-cp39-cp39-win32.whl", hash = "sha256:47d31bdf58222dd296976aa1646c68c6ee80b96d22e0a3c336c9174e253fd35e"},
-    {file = "protobuf-4.22.0-cp39-cp39-win_amd64.whl", hash = "sha256:c27f371f0159feb70e6ea52ed7e768b3f3a4c5676c1900a7e51a24740381650e"},
-    {file = "protobuf-4.22.0-py3-none-any.whl", hash = "sha256:c3325803095fb4c2a48649c321d2fbde59f8fbfcb9bfc7a86df27d112831c571"},
-    {file = "protobuf-4.22.0.tar.gz", hash = "sha256:652d8dfece122a24d98eebfef30e31e455d300efa41999d1182e015984ac5930"},
+    {file = "protobuf-4.23.3-cp310-abi3-win32.whl", hash = "sha256:514b6bbd54a41ca50c86dd5ad6488afe9505901b3557c5e0f7823a0cf67106fb"},
+    {file = "protobuf-4.23.3-cp310-abi3-win_amd64.whl", hash = "sha256:cc14358a8742c4e06b1bfe4be1afbdf5c9f6bd094dff3e14edb78a1513893ff5"},
+    {file = "protobuf-4.23.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:2991f5e7690dab569f8f81702e6700e7364cc3b5e572725098215d3da5ccc6ac"},
+    {file = "protobuf-4.23.3-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:08fe19d267608d438aa37019236db02b306e33f6b9902c3163838b8e75970223"},
+    {file = "protobuf-4.23.3-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:3b01a5274ac920feb75d0b372d901524f7e3ad39c63b1a2d55043f3887afe0c1"},
+    {file = "protobuf-4.23.3-cp37-cp37m-win32.whl", hash = "sha256:aca6e86a08c5c5962f55eac9b5bd6fce6ed98645d77e8bfc2b952ecd4a8e4f6a"},
+    {file = "protobuf-4.23.3-cp37-cp37m-win_amd64.whl", hash = "sha256:0149053336a466e3e0b040e54d0b615fc71de86da66791c592cc3c8d18150bf8"},
+    {file = "protobuf-4.23.3-cp38-cp38-win32.whl", hash = "sha256:84ea0bd90c2fdd70ddd9f3d3fc0197cc24ecec1345856c2b5ba70e4d99815359"},
+    {file = "protobuf-4.23.3-cp38-cp38-win_amd64.whl", hash = "sha256:3bcbeb2bf4bb61fe960dd6e005801a23a43578200ea8ceb726d1f6bd0e562ba1"},
+    {file = "protobuf-4.23.3-cp39-cp39-win32.whl", hash = "sha256:5cb9e41188737f321f4fce9a4337bf40a5414b8d03227e1d9fbc59bc3a216e35"},
+    {file = "protobuf-4.23.3-cp39-cp39-win_amd64.whl", hash = "sha256:29660574cd769f2324a57fb78127cda59327eb6664381ecfe1c69731b83e8288"},
+    {file = "protobuf-4.23.3-py3-none-any.whl", hash = "sha256:447b9786ac8e50ae72cae7a2eec5c5df6a9dbf9aa6f908f1b8bda6032644ea62"},
+    {file = "protobuf-4.23.3.tar.gz", hash = "sha256:7a92beb30600332a52cdadbedb40d33fd7c8a0d7f549c440347bc606fb3fe34b"},
 ]
 
 [[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 = "*"
 files = [
@@ -767,7 +701,6 @@ files = [
 name = "pycodestyle"
 version = "2.7.0"
 description = "Python style guide checker"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -779,7 +712,6 @@ files = [
 name = "pycparser"
 version = "2.21"
 description = "C parser in Python"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -791,7 +723,6 @@ files = [
 name = "pyflakes"
 version = "2.3.1"
 description = "passive checker of Python programs"
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
 files = [
@@ -803,7 +734,6 @@ files = [
 name = "pygments"
 version = "2.14.0"
 description = "Pygments is a syntax highlighting package written in Python."
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -818,7 +748,6 @@ plugins = ["importlib-metadata"]
 name = "pyinstaller"
 version = "5.8.0"
 description = "PyInstaller bundles a Python application and all its dependencies into a single package."
-category = "dev"
 optional = false
 python-versions = "<3.12,>=3.7"
 files = [
@@ -853,7 +782,6 @@ hook-testing = ["execnet (>=1.5.0)", "psutil", "pytest (>=2.7.3)"]
 name = "pyinstaller-hooks-contrib"
 version = "2023.0"
 description = "Community maintained hooks for PyInstaller"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -865,7 +793,6 @@ files = [
 name = "pyserial"
 version = "3.5"
 description = "Python Serial Port Extension"
-category = "main"
 optional = false
 python-versions = "*"
 files = [
@@ -880,7 +807,6 @@ cp2110 = ["hidapi"]
 name = "pyside2"
 version = "5.15.2.1"
 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.11"
 files = [
@@ -899,7 +825,6 @@ shiboken2 = "5.15.2.1"
 name = "pytz"
 version = "2022.7.1"
 description = "World timezone definitions, modern and historical"
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -911,7 +836,6 @@ files = [
 name = "pywin32-ctypes"
 version = "0.2.0"
 description = ""
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -923,7 +847,6 @@ files = [
 name = "requests"
 version = "2.28.2"
 description = "Python HTTP for Humans."
-category = "dev"
 optional = false
 python-versions = ">=3.7, <4"
 files = [
@@ -943,21 +866,19 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
 
 [[package]]
 name = "semver"
-version = "2.13.0"
-description = "Python helper for Semantic Versioning (http://semver.org/)"
-category = "main"
+version = "3.0.1"
+description = "Python helper for Semantic Versioning (https://semver.org)"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+python-versions = ">=3.7"
 files = [
-    {file = "semver-2.13.0-py2.py3-none-any.whl", hash = "sha256:ced8b23dceb22134307c1b8abfa523da14198793d9787ac838e70e29e77458d4"},
-    {file = "semver-2.13.0.tar.gz", hash = "sha256:fa0fe2722ee1c3f57eac478820c3a5ae2f624af8264cbdf9000c980ff7f75e3f"},
+    {file = "semver-3.0.1-py3-none-any.whl", hash = "sha256:2a23844ba1647362c7490fe3995a86e097bb590d16f0f32dfc383008f19e4cdf"},
+    {file = "semver-3.0.1.tar.gz", hash = "sha256:9ec78c5447883c67b97f98c3b6212796708191d22e4ad30f4570f840171cbce1"},
 ]
 
 [[package]]
 name = "setuptools"
 version = "67.3.3"
 description = "Easily download, build, install, upgrade, and uninstall Python packages"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -974,7 +895,6 @@ testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (
 name = "shiboken2"
 version = "5.15.2.1"
 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.11"
 files = [
@@ -990,7 +910,6 @@ files = [
 name = "six"
 version = "1.16.0"
 description = "Python 2 and 3 compatibility utilities"
-category = "main"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -1002,7 +921,6 @@ files = [
 name = "snowballstemmer"
 version = "2.2.0"
 description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
-category = "dev"
 optional = false
 python-versions = "*"
 files = [
@@ -1014,7 +932,6 @@ files = [
 name = "sphinx"
 version = "5.3.0"
 description = "Python documentation generator"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1050,7 +967,6 @@ test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"]
 name = "sphinx-rtd-theme"
 version = "1.2.0"
 description = "Read the Docs theme for Sphinx"
-category = "dev"
 optional = false
 python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7"
 files = [
@@ -1070,7 +986,6 @@ dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"]
 name = "sphinxcontrib-applehelp"
 version = "1.0.2"
 description = "sphinxcontrib-applehelp is a sphinx extension which outputs Apple help books"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -1086,7 +1001,6 @@ test = ["pytest"]
 name = "sphinxcontrib-autoprogram"
 version = "0.1.8"
 description = "Documenting CLI programs"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1102,7 +1016,6 @@ Sphinx = ">=1.2"
 name = "sphinxcontrib-devhelp"
 version = "1.0.2"
 description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp document."
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -1118,7 +1031,6 @@ test = ["pytest"]
 name = "sphinxcontrib-htmlhelp"
 version = "2.0.0"
 description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1134,7 +1046,6 @@ test = ["html5lib", "pytest"]
 name = "sphinxcontrib-jquery"
 version = "2.0.0"
 description = "Extension to include jQuery on newer Sphinx releases"
-category = "dev"
 optional = false
 python-versions = ">=2.7"
 files = [
@@ -1149,7 +1060,6 @@ setuptools = "*"
 name = "sphinxcontrib-jsmath"
 version = "1.0.1"
 description = "A sphinx extension which renders display math in HTML via JavaScript"
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -1164,7 +1074,6 @@ test = ["flake8", "mypy", "pytest"]
 name = "sphinxcontrib-qthelp"
 version = "1.0.3"
 description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp document."
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -1180,7 +1089,6 @@ test = ["pytest"]
 name = "sphinxcontrib-serializinghtml"
 version = "1.1.5"
 description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)."
-category = "dev"
 optional = false
 python-versions = ">=3.5"
 files = [
@@ -1196,7 +1104,6 @@ test = ["pytest"]
 name = "toml"
 version = "0.10.2"
 description = "Python Library for Tom's Obvious, Minimal Language"
-category = "dev"
 optional = false
 python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
 files = [
@@ -1208,7 +1115,6 @@ files = [
 name = "tomli"
 version = "2.0.1"
 description = "A lil' TOML parser"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1220,7 +1126,6 @@ files = [
 name = "typed-ast"
 version = "1.5.4"
 description = "a fork of Python 2 and 3 ast modules with type comment support"
-category = "dev"
 optional = false
 python-versions = ">=3.6"
 files = [
@@ -1254,7 +1159,6 @@ files = [
 name = "typing-extensions"
 version = "4.5.0"
 description = "Backported and Experimental Type Hints for Python 3.7+"
-category = "main"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1266,7 +1170,6 @@ files = [
 name = "urllib3"
 version = "1.26.14"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
-category = "dev"
 optional = false
 python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
 files = [
@@ -1283,7 +1186,6 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
 name = "zipp"
 version = "3.14.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
-category = "dev"
 optional = false
 python-versions = ">=3.7"
 files = [
@@ -1301,4 +1203,4 @@ qt = ["pyside2"]
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.7,<3.12"
-content-hash = "379a1a94b266321aa1b2b60fc4367e2ec37f34a6b99cdd031fe73d0fb45c2ed5"
+content-hash = "c45296e342a731894754db4cf7ca5352d1aa7ab6d3bf20028a15c8ed47c0045c"
diff --git a/pyproject.toml b/pyproject.toml
index 31510130f..e5ed3f0e3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -17,17 +17,19 @@ packages = [
 
 [tool.poetry.dependencies]
 python = "^3.7,<3.12"
-hidapi = "~0"
+hidapi = ">=0.14.0"
 ecdsa = "~0"
 pyaes = "^1.6"
 mnemonic = "~0"
 typing-extensions = "^4.4"
 libusb1 = ">=1.7,<3"
 pyside2 = { version = "^5.14.0", optional = true, python = "<3.10" }
-bitbox02 = ">=6.1.0,<7.0.0"
 cbor = "^1.0.0"
 pyserial = "^3.5"
 dataclasses = {version = "^0.8", python = ">=3.6,<3.7"}
+semver = "^3.0.1"
+noiseprotocol = "^0.3.1"
+protobuf = "^4.23.3"
 
 [tool.poetry.extras]
 qt = ["pyside2"]


Test patch:
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/perform_attestation_pb2.py b/hwilib/devices/bitbox02_lib/communication/generated/perform_attestation_pb2.py
new file mode 100644
index 000000000..52af6727c
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/perform_attestation_pb2.py
@@ -0,0 +1,27 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: perform_attestation.proto
+"""Generated protocol buffer code."""
+from google.protobuf.internal import builder as _builder
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x19perform_attestation.proto\x12\x14shiftcrypto.bitbox02\".\n\x19PerformAttestationRequest\x12\x11\n\tchallenge\x18\x01 \x01(\x0c\"\x9e\x01\n\x1aPerformAttestationResponse\x12\x17\n\x0f\x62ootloader_hash\x18\x01 \x01(\x0c\x12\x15\n\rdevice_pubkey\x18\x02 \x01(\x0c\x12\x13\n\x0b\x63\x65rtificate\x18\x03 \x01(\x0c\x12\x1e\n\x16root_pubkey_identifier\x18\x04 \x01(\x0c\x12\x1b\n\x13\x63hallenge_signature\x18\x05 \x01(\x0c\x62\x06proto3')
+
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'perform_attestation_pb2', globals())
+if _descriptor._USE_C_DESCRIPTORS == False:
+
+  DESCRIPTOR._options = None
+  _PERFORMATTESTATIONREQUEST._serialized_start=51
+  _PERFORMATTESTATIONREQUEST._serialized_end=97
+  _PERFORMATTESTATIONRESPONSE._serialized_start=100
+  _PERFORMATTESTATIONRESPONSE._serialized_end=258
+# @@protoc_insertion_point(module_scope)
diff --git a/hwilib/devices/bitbox02_lib/communication/generated/perform_attestation_pb2.pyi b/hwilib/devices/bitbox02_lib/communication/generated/perform_attestation_pb2.pyi
new file mode 100644
index 000000000..830918a3e
--- /dev/null
+++ b/hwilib/devices/bitbox02_lib/communication/generated/perform_attestation_pb2.pyi
@@ -0,0 +1,48 @@
+"""
+@generated by mypy-protobuf.  Do not edit manually!
+isort:skip_file
+"""
+import builtins
+import google.protobuf.descriptor
+import google.protobuf.message
+import typing_extensions
+
+DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
+
+class PerformAttestationRequest(google.protobuf.message.Message):
+    """Deprecated, last used in v1.0.0"""
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    CHALLENGE_FIELD_NUMBER: builtins.int
+    challenge: builtins.bytes
+    """32 bytes challenge."""
+
+    def __init__(self,
+        *,
+        challenge: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["challenge",b"challenge"]) -> None: ...
+global___PerformAttestationRequest = PerformAttestationRequest
+
+class PerformAttestationResponse(google.protobuf.message.Message):
+    """Deprecated, last used in v1.0.0"""
+    DESCRIPTOR: google.protobuf.descriptor.Descriptor
+    BOOTLOADER_HASH_FIELD_NUMBER: builtins.int
+    DEVICE_PUBKEY_FIELD_NUMBER: builtins.int
+    CERTIFICATE_FIELD_NUMBER: builtins.int
+    ROOT_PUBKEY_IDENTIFIER_FIELD_NUMBER: builtins.int
+    CHALLENGE_SIGNATURE_FIELD_NUMBER: builtins.int
+    bootloader_hash: builtins.bytes
+    device_pubkey: builtins.bytes
+    certificate: builtins.bytes
+    root_pubkey_identifier: builtins.bytes
+    challenge_signature: builtins.bytes
+    def __init__(self,
+        *,
+        bootloader_hash: builtins.bytes = ...,
+        device_pubkey: builtins.bytes = ...,
+        certificate: builtins.bytes = ...,
+        root_pubkey_identifier: builtins.bytes = ...,
+        challenge_signature: builtins.bytes = ...,
+        ) -> None: ...
+    def ClearField(self, field_name: typing_extensions.Literal["bootloader_hash",b"bootloader_hash","certificate",b"certificate","challenge_signature",b"challenge_signature","device_pubkey",b"device_pubkey","root_pubkey_identifier",b"root_pubkey_identifier"]) -> None: ...
+global___PerformAttestationResponse = PerformAttestationResponse