add BitBox02 support
This commit is contained in:
parent
1d0bbb6281
commit
4e4455d2b2
44
README.md
44
README.md
@ -90,29 +90,29 @@ The below table lists what devices and features are supported for each device.
|
||||
|
||||
Please also see [docs](docs/) for additional information about each device.
|
||||
|
||||
| Feature \ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | Digital BitBox | KeepKey | Coldcard |
|
||||
| Feature \ Device | Ledger Nano X | Ledger Nano S | Trezor One | Trezor Model T | BitBox01 | BitBox02 | KeepKey | Coldcard |
|
||||
|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:|
|
||||
| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Message Signing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | N/A |
|
||||
| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | N/A |
|
||||
| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | N/A |
|
||||
| Device Backup | N/A | N/A | N/A | N/A | Yes | N/A | Yes |
|
||||
| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||
| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||
| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||
| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A |
|
||||
| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes |
|
||||
| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| Support Planned | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Implemented | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| xpub retrieval | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Message Signing | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| Device Setup | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |
|
||||
| Device Wipe | N/A | N/A | Yes | Yes | Yes | Yes | Yes | N/A |
|
||||
| Device Recovery | N/A | N/A | Yes | Yes | N/A | Yes | Yes | N/A |
|
||||
| Device Backup | N/A | N/A | N/A | N/A | Yes | Yes | N/A | Yes |
|
||||
| P2PKH Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| P2SH-P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2WPKH Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| P2SH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| P2SH-P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| P2WSH Multisig Inputs | Yes | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Bare Multisig Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |
|
||||
| Arbitrary scriptPubKey Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |
|
||||
| Arbitrary redeemScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |
|
||||
| Arbitrary witnessScript Inputs | Yes | Yes | N/A | N/A | Yes | N/A | N/A | N/A |
|
||||
| Non-wallet inputs | Yes | Yes | Yes | Yes | Yes | N/A | Yes | Yes |
|
||||
| Mixed Segwit and Non-Segwit Inputs | N/A | N/A | Yes | Yes | Yes | Yes | Yes | Yes |
|
||||
| Display on device screen | Yes | Yes | Yes | Yes | N/A | Yes | Yes | Yes |
|
||||
|
||||
## Using with Bitcoin Core
|
||||
|
||||
|
||||
49
docs/bitbox02.md
Normal file
49
docs/bitbox02.md
Normal file
@ -0,0 +1,49 @@
|
||||
# BitBox02
|
||||
|
||||
The BitBox02 is supported by HWI.
|
||||
|
||||
Current implemented commands are:
|
||||
|
||||
* `signtx`
|
||||
* `getxpub`
|
||||
* `displayaddress`
|
||||
* `setup`
|
||||
* `wipe`
|
||||
* `restore`
|
||||
* `backup`
|
||||
* `togglepassphrase`
|
||||
|
||||
Multisig (P2WSH only) is supported by the BitBox02, but is not ingerated into HWI yet. Coming
|
||||
soon^{tm}.
|
||||
|
||||
# Usage Notes
|
||||
|
||||
## Strict keypaths
|
||||
|
||||
The BitBox02 has strict keypath validation.
|
||||
|
||||
The only accepted keypaths for xpubs are:
|
||||
|
||||
- `m/49'/0'/<account'>` for `p2wpkh-p2sh` (segwit wrapped in P2SH)
|
||||
- `m/84'/0'/<account'>` for `p2wpkh` (native segwit v0)
|
||||
- `m/48'/0'/<account'>/2` for p2wsh multisig (native segwit v0 multisig).
|
||||
|
||||
`account'` can be between `0'` and `99'`.
|
||||
|
||||
For address keypaths, append `/0/<address index>` for a receive and `/1/<change index>` for a change
|
||||
address. Up to `10000` addresses are supported.
|
||||
|
||||
In `--testnet` mode, the second element must be `1'` (e.g. `m/49'/1'/...`).
|
||||
|
||||
## Signing with mixed input types
|
||||
|
||||
The BitBox02 allows mixing inputs of different script types (e.g. and `p2wpkh-p2sh` `p2wpkh`), as
|
||||
long as the keypaths use the appropriate bip44 purpose field per input (e.g. `49'` and `84'`) and
|
||||
all account indexes are the same.
|
||||
|
||||
Multisig and singlesig inputs cannot be mixed.
|
||||
|
||||
## getmasterxpub and legacy addresses not supported
|
||||
|
||||
`getmasterxpub` is the same as `getxpub` at the legacy keypath `m/44'/0'/0'`. Legacy xpub, addresses
|
||||
and inputs are not supported.
|
||||
@ -9,6 +9,7 @@ from .serializations import PSBT
|
||||
from .base58 import xpub_to_pub_hex
|
||||
from .errors import (
|
||||
UnknownDeviceError,
|
||||
UnavailableActionError,
|
||||
BAD_ARGUMENT,
|
||||
NOT_IMPLEMENTED,
|
||||
)
|
||||
@ -206,10 +207,12 @@ def getdescriptors(client, account=0):
|
||||
|
||||
for internal in [False, True]:
|
||||
descriptors = []
|
||||
desc1 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.PKH, account=account)
|
||||
desc2 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.SH_WPKH, account=account)
|
||||
desc3 = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=AddressType.WPKH, account=account)
|
||||
for desc in [desc1, desc2, desc3]:
|
||||
for addr_type in (AddressType.PKH, AddressType.SH_WPKH, AddressType.WPKH):
|
||||
try:
|
||||
desc = getdescriptor(client, master_fpr=master_fpr, testnet=client.is_testnet, internal=internal, addr_type=addr_type, account=account)
|
||||
except UnavailableActionError:
|
||||
# Device does not support this address type or network. Skip.
|
||||
continue
|
||||
if not isinstance(desc, Descriptor):
|
||||
return desc
|
||||
descriptors.append(desc.serialize())
|
||||
|
||||
@ -3,5 +3,6 @@ __all__ = [
|
||||
'ledger',
|
||||
'keepkey',
|
||||
'digitalbitbox',
|
||||
'coldcard'
|
||||
'coldcard',
|
||||
'bitbox02',
|
||||
]
|
||||
|
||||
613
hwilib/devices/bitbox02.py
Normal file
613
hwilib/devices/bitbox02.py
Normal file
@ -0,0 +1,613 @@
|
||||
from typing import (
|
||||
cast,
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Optional,
|
||||
Union,
|
||||
Tuple,
|
||||
List,
|
||||
Sequence,
|
||||
TypeVar,
|
||||
)
|
||||
from binascii import unhexlify
|
||||
import struct
|
||||
import builtins
|
||||
import sys
|
||||
from functools import wraps
|
||||
|
||||
from ..hwwclient import HardwareWalletClient, Descriptor
|
||||
from ..serializations import (
|
||||
PSBT,
|
||||
CTxOut,
|
||||
is_p2pkh,
|
||||
is_p2wpkh,
|
||||
is_p2wsh,
|
||||
ser_uint256,
|
||||
ser_sig_der,
|
||||
)
|
||||
from ..errors import (
|
||||
HWWError,
|
||||
ActionCanceledError,
|
||||
BadArgumentError,
|
||||
DeviceNotReadyError,
|
||||
UnavailableActionError,
|
||||
DEVICE_NOT_INITIALIZED,
|
||||
handle_errors,
|
||||
common_err_msgs,
|
||||
)
|
||||
|
||||
import hid # type: ignore
|
||||
|
||||
from .trezorlib.tools import parse_path
|
||||
|
||||
from bitbox02 import util
|
||||
from bitbox02 import bitbox02
|
||||
from bitbox02.communication import (
|
||||
devices,
|
||||
u2fhid,
|
||||
FirmwareVersionOutdatedException,
|
||||
Bitbox02Exception,
|
||||
UserAbortException,
|
||||
HARDENED,
|
||||
ERR_GENERIC,
|
||||
)
|
||||
|
||||
from bitbox02.communication.bitbox_api_protocol import (
|
||||
Platform,
|
||||
BitBox02Edition,
|
||||
BitBoxNoiseConfig,
|
||||
)
|
||||
|
||||
|
||||
class BitBox02Error(UnavailableActionError):
|
||||
def __init__(self, msg: str):
|
||||
"""
|
||||
BitBox02 unexpected error. The BitBox02 does not return give granular error messages,
|
||||
so we give hints to as what could be wrong.
|
||||
"""
|
||||
msg = "Input error: {}. A keypath might be invalid. Supported keypaths are: ".format(
|
||||
msg
|
||||
)
|
||||
msg += "m/49'/0'/<account'> for p2wpkh-p2sh; "
|
||||
msg += "m/84'/0'/<account'> for p2wpkh; "
|
||||
msg += "m/48'/0'/<account'>/2' for p2wsh multisig; "
|
||||
msg += "account can be between 0' and 99'; "
|
||||
msg += "For address keypaths, append /0/<address index> for a receive and /1/<change index> for a change address."
|
||||
super().__init__(msg)
|
||||
|
||||
|
||||
ERR_INVALID_INPUT = 101
|
||||
|
||||
PURPOSE_P2WPKH_P2SH = 49 + HARDENED
|
||||
PURPOSE_P2WPKH = 84 + HARDENED
|
||||
PURPOSE_MULTISIG_P2WSH = 48 + HARDENED
|
||||
|
||||
# External GUI tools using hwi.py as a command line tool to integrate hardware wallets usually do
|
||||
# not have an actual terminal for IO.
|
||||
_using_external_gui = not sys.stdout.isatty()
|
||||
if _using_external_gui:
|
||||
_unpaired_errmsg = "Device not paired yet. Please pair using the BitBoxApp, then close the BitBoxApp and try again."
|
||||
else:
|
||||
_unpaired_errmsg = "Device not paired yet. Please use any subcommand to pair"
|
||||
|
||||
|
||||
class SilentNoiseConfig(util.BitBoxAppNoiseConfig):
|
||||
"""
|
||||
Used during `enumerate()`. Raises an exception if the device is unpaired.
|
||||
Attestation check is silent.
|
||||
|
||||
Rationale: enumerate() should not show any dialogs.
|
||||
"""
|
||||
|
||||
def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:
|
||||
raise DeviceNotReadyError(_unpaired_errmsg)
|
||||
|
||||
def attestation_check(self, result: bool) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class CLINoiseConfig(util.BitBoxAppNoiseConfig):
|
||||
""" Noise pairing and attestation check handling in the terminal (stdin/stdout) """
|
||||
|
||||
def show_pairing(self, code: str, device_response: Callable[[], bool]) -> bool:
|
||||
if _using_external_gui:
|
||||
# The user can't see the pairing in the terminal. The
|
||||
# output format is also not appropriate for parsing by
|
||||
# external tools doing inter process communication using
|
||||
# stdin/stdout. For now, we direct the user to pair in the
|
||||
# BitBoxApp instead.
|
||||
raise DeviceNotReadyError(_unpaired_errmsg)
|
||||
|
||||
print("Please compare and confirm the pairing code on your BitBox02:")
|
||||
print(code)
|
||||
if not device_response():
|
||||
return False
|
||||
return input("Accept pairing? [y]/n: ").strip() != "n"
|
||||
|
||||
def attestation_check(self, result: bool) -> None:
|
||||
if result:
|
||||
sys.stderr.write("BitBox02 attestation check PASSED\n")
|
||||
else:
|
||||
sys.stderr.write("BitBox02 attestation check FAILED\n")
|
||||
sys.stderr.write(
|
||||
"Your BitBox02 might not be genuine. Please contact support@shiftcrypto.ch if the problem persists.\n"
|
||||
)
|
||||
|
||||
|
||||
def enumerate(password: str = "") -> List[Dict[str, object]]:
|
||||
"""
|
||||
Enumerate all BitBox02 devices. Bootloaders excluded.
|
||||
"""
|
||||
result = []
|
||||
for device_info in devices.get_any_bitbox02s():
|
||||
path = device_info["path"].decode()
|
||||
client = Bitbox02Client(path)
|
||||
client.set_noise_config(SilentNoiseConfig())
|
||||
version, platform, edition, unlocked = bitbox02.BitBox02.get_info(
|
||||
client.transport
|
||||
)
|
||||
if platform != Platform.BITBOX02:
|
||||
client.close()
|
||||
continue
|
||||
if edition not in (BitBox02Edition.MULTI, BitBox02Edition.BTCONLY):
|
||||
client.close()
|
||||
continue
|
||||
|
||||
assert isinstance(edition, BitBox02Edition)
|
||||
|
||||
d_data = {
|
||||
"type": "bitbox02",
|
||||
"path": path,
|
||||
"model": {
|
||||
BitBox02Edition.MULTI: "bitbox02_multi",
|
||||
BitBox02Edition.BTCONLY: "bitbox02_btconly",
|
||||
}[edition],
|
||||
"needs_pin_sent": False,
|
||||
"needs_passphrase_sent": False,
|
||||
}
|
||||
|
||||
with handle_errors(common_err_msgs["enumerate"], d_data):
|
||||
if not unlocked:
|
||||
raise DeviceNotReadyError(
|
||||
"Please load wallet to unlock."
|
||||
if _using_external_gui
|
||||
else "Please use any subcommand to unlock"
|
||||
)
|
||||
bb02 = client.init()
|
||||
info = bb02.device_info()
|
||||
if not info["initialized"]:
|
||||
raise HWWError("Not initialized", DEVICE_NOT_INITIALIZED)
|
||||
d_data["fingerprint"] = client.get_master_fingerprint_hex()
|
||||
|
||||
result.append(d_data)
|
||||
|
||||
client.close()
|
||||
return result
|
||||
|
||||
|
||||
T = TypeVar("T", bound=Callable[..., Any])
|
||||
|
||||
|
||||
def bitbox02_exception(f: T) -> T:
|
||||
"""
|
||||
Maps bitbox02 library exceptions into a HWI exceptions.
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def func(*args, **kwargs): # type: ignore
|
||||
""" Wraps f, mapping exceptions. """
|
||||
try:
|
||||
return f(*args, **kwargs)
|
||||
except UserAbortException:
|
||||
raise ActionCanceledError("{} canceled".format(f.__name__))
|
||||
except Bitbox02Exception as exc:
|
||||
if exc.code in (ERR_GENERIC, ERR_INVALID_INPUT):
|
||||
raise BitBox02Error(str(exc))
|
||||
raise exc
|
||||
except FirmwareVersionOutdatedException as exc:
|
||||
raise DeviceNotReadyError(str(exc))
|
||||
|
||||
return cast(T, func)
|
||||
|
||||
|
||||
# This class extends the HardwareWalletClient for BitBox02 specific things
|
||||
class Bitbox02Client(HardwareWalletClient):
|
||||
def __init__(self, path: str, password: str = "", expert: bool = False) -> None:
|
||||
"""
|
||||
Initializes a new BitBox02 client instance.
|
||||
"""
|
||||
super().__init__(path, password=password, expert=expert)
|
||||
if password != "":
|
||||
raise BadArgumentError(
|
||||
"The BitBox02 does not accept a passphrase from the host. Please enable the passphrase option and enter the passphrase on the device during unlock."
|
||||
)
|
||||
|
||||
hid_device = hid.device()
|
||||
hid_device.open_path(path.encode())
|
||||
self.transport = u2fhid.U2FHid(hid_device)
|
||||
self.device_path = path
|
||||
|
||||
# use self.init() to access self.bb02.
|
||||
self.bb02: Optional[bitbox02.BitBox02] = None
|
||||
|
||||
self.noise_config: BitBoxNoiseConfig = CLINoiseConfig()
|
||||
|
||||
def set_noise_config(self, noise_config: BitBoxNoiseConfig) -> None:
|
||||
self.noise_config = noise_config
|
||||
|
||||
def init(self) -> bitbox02.BitBox02:
|
||||
if self.bb02 is not None:
|
||||
return self.bb02
|
||||
|
||||
for device_info in devices.get_any_bitbox02s():
|
||||
if device_info["path"].decode() == self.device_path:
|
||||
bb02 = bitbox02.BitBox02(
|
||||
transport=self.transport,
|
||||
device_info=device_info,
|
||||
noise_config=self.noise_config,
|
||||
)
|
||||
try:
|
||||
bb02.check_min_version()
|
||||
except FirmwareVersionOutdatedException as exc:
|
||||
sys.stderr.write("WARNING: {}\n".format(exc))
|
||||
raise
|
||||
self.bb02 = bb02
|
||||
return bb02
|
||||
raise Exception(
|
||||
"Could not find the hid device info for path {}".format(self.device_path)
|
||||
)
|
||||
|
||||
def close(self) -> None:
|
||||
self.transport.close()
|
||||
|
||||
def get_master_fingerprint_hex(self) -> str:
|
||||
"""
|
||||
HWI by default retrieves the fingerprint at m/ by getting the xpub at m/0', which contains the parent fingerprint.
|
||||
The BitBox02 does not support querying arbitrary keypaths, but has an api call return the fingerprint at m/.
|
||||
"""
|
||||
bb02 = self.init()
|
||||
if not bb02.device_info()["initialized"]:
|
||||
raise UnavailableActionError("Not initialized")
|
||||
return bb02.root_fingerprint().hex()
|
||||
|
||||
def prompt_pin(self) -> Dict[str, Union[bool, str, int]]:
|
||||
raise UnavailableActionError(
|
||||
"The BitBox02 does not need a PIN sent from the host"
|
||||
)
|
||||
|
||||
def send_pin(self, pin: str) -> Dict[str, Union[bool, str, int]]:
|
||||
raise UnavailableActionError(
|
||||
"The BitBox02 does not need a PIN sent from the host"
|
||||
)
|
||||
|
||||
def _get_coin(self) -> bitbox02.btc.BTCCoin:
|
||||
if self.is_testnet:
|
||||
return bitbox02.btc.TBTC
|
||||
return bitbox02.btc.BTC
|
||||
|
||||
def _get_xpub(self, keypath: Sequence[int]) -> str:
|
||||
xpub_type = (
|
||||
bitbox02.btc.BTCPubRequest.TPUB
|
||||
if self.is_testnet
|
||||
else bitbox02.btc.BTCPubRequest.XPUB
|
||||
)
|
||||
return self.init().btc_xpub(
|
||||
keypath, coin=self._get_coin(), xpub_type=xpub_type, display=False
|
||||
)
|
||||
|
||||
def get_pubkey_at_path(self, bip32_path: str) -> Dict[str, str]:
|
||||
path_uint32s = parse_path(bip32_path)
|
||||
try:
|
||||
xpub = self._get_xpub(path_uint32s)
|
||||
except Bitbox02Exception as exc:
|
||||
raise BitBox02Error(str(exc))
|
||||
return {"xpub": xpub}
|
||||
|
||||
@bitbox02_exception
|
||||
def display_address(
|
||||
self,
|
||||
bip32_path: str,
|
||||
p2sh_p2wpkh: bool,
|
||||
bech32: bool,
|
||||
redeem_script: Optional[str] = None,
|
||||
descriptor: Optional[Descriptor] = None,
|
||||
) -> Dict[str, str]:
|
||||
if redeem_script:
|
||||
raise NotImplementedError("BitBox02 multisig not integrated into HWI yet")
|
||||
|
||||
if p2sh_p2wpkh:
|
||||
script_config = bitbox02.btc.BTCScriptConfig(
|
||||
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
|
||||
)
|
||||
elif bech32:
|
||||
script_config = bitbox02.btc.BTCScriptConfig(
|
||||
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH
|
||||
)
|
||||
else:
|
||||
raise UnavailableActionError(
|
||||
"The BitBox02 does not support legacy p2pkh addresses"
|
||||
)
|
||||
address = self.init().btc_address(
|
||||
parse_path(bip32_path),
|
||||
coin=self._get_coin(),
|
||||
script_config=script_config,
|
||||
display=True,
|
||||
)
|
||||
return {"address": address}
|
||||
|
||||
@bitbox02_exception
|
||||
def sign_tx(self, psbt: PSBT) -> Dict[str, str]:
|
||||
def find_our_key(
|
||||
keypaths: Dict[bytes, Sequence[int]]
|
||||
) -> Tuple[Optional[bytes], Optional[Sequence[int]]]:
|
||||
"""
|
||||
Keypaths is a map of pubkey to hd keypath, where the first element in the keypath is the master fingerprint. We attempt to find the key which belongs to the BitBox02 by matching the fingerprint, and then matching the pubkey.
|
||||
Returns the pubkey and the keypath, without the fingerprint.
|
||||
"""
|
||||
for pubkey, keypath_with_fingerprint in keypaths.items():
|
||||
fp, keypath = keypath_with_fingerprint[0], keypath_with_fingerprint[1:]
|
||||
# Cheap check if the key is ours.
|
||||
if fp != master_fp:
|
||||
continue
|
||||
|
||||
# Expensive check if the key is ours.
|
||||
# TODO: check for fingerprint collision
|
||||
# keypath_account = keypath[:-2]
|
||||
|
||||
return pubkey, keypath
|
||||
return None, None
|
||||
|
||||
def get_simple_type(
|
||||
output: CTxOut, redeem_script: bytes
|
||||
) -> bitbox02.btc.BTCScriptConfig.SimpleType:
|
||||
if is_p2pkh(output.scriptPubKey):
|
||||
raise BadArgumentError(
|
||||
"The BitBox02 does not support legacy p2pkh scripts"
|
||||
)
|
||||
if is_p2wpkh(output.scriptPubKey):
|
||||
return bitbox02.btc.BTCScriptConfig.P2WPKH
|
||||
if output.is_p2sh() and is_p2wpkh(redeem_script):
|
||||
return bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
|
||||
raise BadArgumentError(
|
||||
"Input script type not recognized of input {}.".format(input_index)
|
||||
)
|
||||
|
||||
master_fp = struct.unpack("<I", unhexlify(self.get_master_fingerprint_hex()))[0]
|
||||
|
||||
inputs: List[bitbox02.BTCInputType] = []
|
||||
|
||||
bip44_account = None
|
||||
|
||||
# One pubkey per input. The pubkey identifies the key per input with which we sign. There
|
||||
# must be exactly one pubkey per input that belongs to the BitBox02.
|
||||
found_pubkeys: List[bytes] = []
|
||||
|
||||
for input_index, (psbt_in, tx_in) in builtins.enumerate(
|
||||
zip(psbt.inputs, psbt.tx.vin)
|
||||
):
|
||||
if psbt_in.sighash and psbt_in.sighash != 1:
|
||||
raise BadArgumentError(
|
||||
"The BitBox02 only supports SIGHASH_ALL. Found sighash: {}".format(
|
||||
psbt_in.sighash
|
||||
)
|
||||
)
|
||||
|
||||
utxo = None
|
||||
prevtx = None
|
||||
|
||||
# psbt_in.witness_utxo was originally used for segwit utxo's, but since it was
|
||||
# discovered that the amounts are not correctly committed to in the segwit sighash, the
|
||||
# full prevtx (non_witness_utxo) is supplied for both segwit and non-segwit inputs.
|
||||
# See
|
||||
# - https://medium.com/shiftcrypto/bitbox-app-firmware-update-6-2020-c70f733a5330
|
||||
# - https://blog.trezor.io/details-of-firmware-updates-for-trezor-one-version-1-9-1-and-trezor-model-t-version-2-3-1-1eba8f60f2dd.
|
||||
# - https://github.com/zkSNACKs/WalletWasabi/pull/3822
|
||||
# The BitBox02 for now requires the prevtx, at least until Taproot activates.
|
||||
|
||||
if psbt_in.non_witness_utxo:
|
||||
if tx_in.prevout.hash != psbt_in.non_witness_utxo.sha256:
|
||||
raise BadArgumentError(
|
||||
"Input {} has a non_witness_utxo with the wrong hash".format(
|
||||
input_index
|
||||
)
|
||||
)
|
||||
utxo = psbt_in.non_witness_utxo.vout[tx_in.prevout.n]
|
||||
prevtx = psbt_in.non_witness_utxo
|
||||
elif psbt_in.witness_utxo:
|
||||
utxo = psbt_in.witness_utxo
|
||||
if utxo is None:
|
||||
raise BadArgumentError("No utxo found for input {}".format(input_index))
|
||||
if prevtx is None:
|
||||
raise BadArgumentError(
|
||||
"Previous transaction missing for input {}".format(input_index)
|
||||
)
|
||||
|
||||
found_pubkey, keypath = find_our_key(psbt_in.hd_keypaths)
|
||||
if not found_pubkey:
|
||||
raise BadArgumentError("No key found for input {}".format(input_index))
|
||||
assert keypath is not None
|
||||
found_pubkeys.append(found_pubkey)
|
||||
|
||||
# TOOD: validate keypath
|
||||
|
||||
if bip44_account is None:
|
||||
bip44_account = keypath[2]
|
||||
elif bip44_account != keypath[2]:
|
||||
raise BadArgumentError(
|
||||
"The bip44 account index must be the same for all inputs and changes"
|
||||
)
|
||||
|
||||
simple_type = get_simple_type(utxo, psbt_in.redeem_script)
|
||||
|
||||
script_config_index_map = {
|
||||
bitbox02.btc.BTCScriptConfig.P2WPKH: 0,
|
||||
bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH: 1,
|
||||
}
|
||||
|
||||
inputs.append(
|
||||
{
|
||||
"prev_out_hash": ser_uint256(tx_in.prevout.hash),
|
||||
"prev_out_index": tx_in.prevout.n,
|
||||
"prev_out_value": utxo.nValue,
|
||||
"sequence": tx_in.nSequence,
|
||||
"keypath": keypath,
|
||||
"script_config_index": script_config_index_map[simple_type],
|
||||
"prev_tx": {
|
||||
"version": prevtx.nVersion,
|
||||
"locktime": prevtx.nLockTime,
|
||||
"inputs": [
|
||||
{
|
||||
"prev_out_hash": ser_uint256(prev_in.prevout.hash),
|
||||
"prev_out_index": prev_in.prevout.n,
|
||||
"signature_script": prev_in.scriptSig,
|
||||
"sequence": prev_in.nSequence,
|
||||
}
|
||||
for prev_in in prevtx.vin
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"value": prev_out.nValue,
|
||||
"pubkey_script": prev_out.scriptPubKey,
|
||||
}
|
||||
for prev_out in prevtx.vout
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
outputs: List[bitbox02.BTCOutputType] = []
|
||||
for output_index, (psbt_out, tx_out) in builtins.enumerate(
|
||||
zip(psbt.outputs, psbt.tx.vout)
|
||||
):
|
||||
_, keypath = find_our_key(psbt_out.hd_keypaths)
|
||||
is_change = keypath and keypath[-2] == 1
|
||||
if is_change:
|
||||
assert keypath is not None
|
||||
simple_type = get_simple_type(tx_out, psbt_out.redeem_script)
|
||||
outputs.append(
|
||||
bitbox02.BTCOutputInternal(
|
||||
keypath=keypath,
|
||||
value=tx_out.nValue,
|
||||
script_config_index=script_config_index_map[simple_type],
|
||||
)
|
||||
)
|
||||
else:
|
||||
if tx_out.is_p2pkh():
|
||||
output_type = bitbox02.btc.P2PKH
|
||||
output_hash = tx_out.scriptPubKey[3:23]
|
||||
elif is_p2wpkh(tx_out.scriptPubKey):
|
||||
output_type = bitbox02.btc.P2WPKH
|
||||
output_hash = tx_out.scriptPubKey[2:]
|
||||
elif tx_out.is_p2sh():
|
||||
output_type = bitbox02.btc.P2SH
|
||||
output_hash = tx_out.scriptPubKey[2:22]
|
||||
elif is_p2wsh(tx_out.scriptPubKey):
|
||||
output_type = bitbox02.btc.P2WSH
|
||||
output_hash = tx_out.scriptPubKey[2:]
|
||||
else:
|
||||
raise BadArgumentError(
|
||||
"Output type not recognized of output {}".format(output_index)
|
||||
)
|
||||
|
||||
outputs.append(
|
||||
bitbox02.BTCOutputExternal(
|
||||
output_type=output_type,
|
||||
output_hash=output_hash,
|
||||
value=tx_out.nValue,
|
||||
)
|
||||
)
|
||||
|
||||
assert bip44_account is not None
|
||||
|
||||
bip44_network = 1 + HARDENED if self.is_testnet else 0 + HARDENED
|
||||
sigs = self.init().btc_sign(
|
||||
bitbox02.btc.TBTC if self.is_testnet else bitbox02.btc.BTC,
|
||||
[
|
||||
bitbox02.btc.BTCScriptConfigWithKeypath(
|
||||
script_config=bitbox02.btc.BTCScriptConfig(
|
||||
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH
|
||||
),
|
||||
keypath=[84 + HARDENED, bip44_network, bip44_account],
|
||||
),
|
||||
bitbox02.btc.BTCScriptConfigWithKeypath(
|
||||
script_config=bitbox02.btc.BTCScriptConfig(
|
||||
simple_type=bitbox02.btc.BTCScriptConfig.P2WPKH_P2SH
|
||||
),
|
||||
keypath=[49 + HARDENED, bip44_network, bip44_account],
|
||||
),
|
||||
],
|
||||
inputs=inputs,
|
||||
outputs=outputs,
|
||||
locktime=psbt.tx.nLockTime,
|
||||
version=psbt.tx.nVersion,
|
||||
)
|
||||
|
||||
for (_, sig), pubkey, psbt_in in zip(sigs, found_pubkeys, psbt.inputs):
|
||||
r, s = sig[:32], sig[32:64]
|
||||
# ser_sig_der() adds SIGHASH_ALL
|
||||
psbt_in.partial_sigs[pubkey] = ser_sig_der(r, s)
|
||||
|
||||
return {"psbt": psbt.serialize()}
|
||||
|
||||
def sign_message(
|
||||
self, message: Union[str, bytes], bip32_path: str
|
||||
) -> Dict[str, str]:
|
||||
raise UnavailableActionError("The BitBox02 does not support 'signmessage'")
|
||||
|
||||
@bitbox02_exception
|
||||
def toggle_passphrase(self) -> Dict[str, Union[bool, str, int]]:
|
||||
bb02 = self.init()
|
||||
info = bb02.device_info()
|
||||
if info["mnemonic_passphrase_enabled"]:
|
||||
bb02.disable_mnemonic_passphrase()
|
||||
else:
|
||||
bb02.enable_mnemonic_passphrase()
|
||||
return {"success": True}
|
||||
|
||||
@bitbox02_exception
|
||||
def setup_device(
|
||||
self, label: str = "", passphrase: str = ""
|
||||
) -> Dict[str, Union[bool, str, int]]:
|
||||
if passphrase:
|
||||
raise UnavailableActionError(
|
||||
"Passphrase not needed when setting up a BitBox02."
|
||||
)
|
||||
|
||||
bb02 = self.init()
|
||||
if bb02.device_info()["initialized"]:
|
||||
raise UnavailableActionError("The BitBox02 must be wiped before setup.")
|
||||
|
||||
if label:
|
||||
bb02.set_device_name(label)
|
||||
if not bb02.set_password():
|
||||
return {"success": False}
|
||||
return {"success": bb02.create_backup()}
|
||||
|
||||
@bitbox02_exception
|
||||
def wipe_device(self) -> Dict[str, Union[bool, str, int]]:
|
||||
return {"success": self.init().reset()}
|
||||
|
||||
@bitbox02_exception
|
||||
def backup_device(
|
||||
self, label: str = "", passphrase: str = ""
|
||||
) -> Dict[str, Union[bool, str, int]]:
|
||||
if label or passphrase:
|
||||
raise UnavailableActionError(
|
||||
"Label/passphrase not needed when exporting mnemonic from the BitBox02."
|
||||
)
|
||||
|
||||
return {"success": self.init().show_mnemonic()}
|
||||
|
||||
@bitbox02_exception
|
||||
def restore_device(
|
||||
self, label: str = "", word_count: int = 24
|
||||
) -> Dict[str, Union[bool, str, int]]:
|
||||
bb02 = self.init()
|
||||
if bb02.device_info()["initialized"]:
|
||||
raise UnavailableActionError("The BitBox02 must be wiped before restore.")
|
||||
|
||||
if label:
|
||||
bb02.set_device_name(label)
|
||||
|
||||
return {"success": bb02.restore_from_mnemonic()}
|
||||
@ -3,12 +3,14 @@
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from typing import Callable
|
||||
|
||||
from . import commands, __version__
|
||||
from .cli import HWIArgumentParser
|
||||
from .errors import handle_errors, DEVICE_NOT_INITIALIZED
|
||||
|
||||
try:
|
||||
from .ui.ui_bitbox02pairing import Ui_BitBox02PairingDialog
|
||||
from .ui.ui_displayaddressdialog import Ui_DisplayAddressDialog
|
||||
from .ui.ui_getxpubdialog import Ui_GetXpubDialog
|
||||
from .ui.ui_getkeypooloptionsdialog import Ui_GetKeypoolOptionsDialog
|
||||
@ -23,7 +25,9 @@ except ImportError:
|
||||
|
||||
from PySide2.QtGui import QRegExpValidator
|
||||
from PySide2.QtWidgets import QApplication, QDialog, QDialogButtonBox, QLineEdit, QMessageBox, QMainWindow
|
||||
from PySide2.QtCore import QRegExp, Signal, Slot
|
||||
from PySide2.QtCore import QCoreApplication, QRegExp, Signal, Slot
|
||||
|
||||
import bitbox02.util
|
||||
|
||||
def do_command(f, *args, **kwargs):
|
||||
result = {}
|
||||
@ -207,6 +211,41 @@ class GetKeypoolOptionsDialog(QDialog):
|
||||
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
|
||||
|
||||
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
|
||||
QCoreApplication.processEvents()
|
||||
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.",
|
||||
)
|
||||
|
||||
class HWIQt(QMainWindow):
|
||||
def __init__(self, passphrase='', testnet=False):
|
||||
super(HWIQt, self).__init__()
|
||||
@ -293,7 +332,6 @@ class HWIQt(QMainWindow):
|
||||
|
||||
self.ui.getxpub_button.setEnabled(True)
|
||||
self.ui.signtx_button.setEnabled(True)
|
||||
self.ui.signmsg_button.setEnabled(True)
|
||||
self.ui.display_addr_button.setEnabled(True)
|
||||
self.ui.getkeypool_opts_button.setEnabled(True)
|
||||
|
||||
@ -302,7 +340,12 @@ class HWIQt(QMainWindow):
|
||||
self.client = commands.get_client(self.device_info['model'], self.device_info['path'], self.passphrase)
|
||||
self.client.is_testnet = self.testnet
|
||||
|
||||
self.ui.toggle_passphrase_button.setEnabled(self.device_info['type'] == 'trezor' or self.device_info['type'] == 'keepkey')
|
||||
if self.device_info['type'] == 'bitbox02':
|
||||
self.client.set_noise_config(BitBox02NoiseConfig())
|
||||
|
||||
self.ui.setpass_button.setEnabled(self.device_info['type'] != 'bitbox02')
|
||||
self.ui.signmsg_button.setEnabled(self.device_info['type'] != 'bitbox02')
|
||||
self.ui.toggle_passphrase_button.setEnabled(self.device_info['type'] in ('trezor', 'keepkey', 'bitbox02', ))
|
||||
|
||||
self.get_device_info()
|
||||
|
||||
|
||||
1
hwilib/udev/53-hid-bitbox02.rules
Normal file
1
hwilib/udev/53-hid-bitbox02.rules
Normal file
@ -0,0 +1 @@
|
||||
SUBSYSTEM=="usb", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02_%n", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403"
|
||||
1
hwilib/udev/54-hid-bitbox02.rules
Normal file
1
hwilib/udev/54-hid-bitbox02.rules
Normal file
@ -0,0 +1 @@
|
||||
KERNEL=="hidraw*", SUBSYSTEM=="hidraw", ATTRS{idVendor}=="03eb", ATTRS{idProduct}=="2403", TAG+="uaccess", TAG+="udev-acl", SYMLINK+="bitbox02-%n"
|
||||
120
hwilib/ui/bitbox02pairing.ui
Normal file
120
hwilib/ui/bitbox02pairing.ui
Normal file
@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>BitBox02PairingDialog</class>
|
||||
<widget class="QDialog" name="BitBox02PairingDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::WindowModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>400</width>
|
||||
<height>209</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Dialog</string>
|
||||
</property>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>30</x>
|
||||
<y>160</y>
|
||||
<width>341</width>
|
||||
<height>32</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::No|QDialogButtonBox::Yes</set>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="pairingCode">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>20</x>
|
||||
<y>80</y>
|
||||
<width>331</width>
|
||||
<height>61</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>DejaVu Sans Mono</family>
|
||||
<pointsize>15</pointsize>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::RichText</enum>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>20</x>
|
||||
<y>10</y>
|
||||
<width>351</width>
|
||||
<height>61</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<pointsize>11</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Please verify the pairing code matches what is
|
||||
shown on your BitBox02.</string>
|
||||
</property>
|
||||
<property name="textFormat">
|
||||
<enum>Qt::PlainText</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>BitBox02PairingDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>BitBox02PairingDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
Loading…
Reference in New Issue
Block a user