Skip to content

Host passphrase #1393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions messages/hww.proto
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ message Request {
ElectrumEncryptionKeyRequest electrum_encryption_key = 26;
CardanoRequest cardano = 27;
BIP85Request bip85 = 28;
UnlockRequest unlock = 29;
UnlockHostInfoRequest unlock_host_info = 30;
}
}

Expand All @@ -89,5 +91,6 @@ message Response {
ElectrumEncryptionKeyResponse electrum_encryption_key = 14;
CardanoResponse cardano = 15;
BIP85Response bip85 = 16;
UnlockRequestHostInfoResponse unlock_host_info = 17;
}
}
21 changes: 21 additions & 0 deletions messages/keystore.proto
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,24 @@ message BIP85Response {
bytes ln = 2;
}
}

message UnlockRequest {
// If true, the device will be allowed to ask the user to enter the passphrase on the host. If
// the user accepts, the host will receive a `UnlockRequestHostInfoResponse` with type
// `PASSPHRASE`.
bool supports_host_passphrase = 1;
}

message UnlockRequestHostInfoResponse {
enum InfoType {
UNKNOWN = 0;
// Respond with `UnlockHostInfoRequest` containing the passphrase.
PASSPHRASE = 1;
}
InfoType type = 1;
}

message UnlockHostInfoRequest {
// Omit if type==PASSPHRASE and entering the passhrase is cancelled on the host.
optional string passphrase = 1;
}
61 changes: 59 additions & 2 deletions py/bitbox02/bitbox02/communication/bitbox_api_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""BitBox02"""

from abc import ABC, abstractmethod
from dataclasses import dataclass
import os
import enum
import sys
Expand All @@ -36,6 +37,7 @@
try:
from .generated import hww_pb2 as hww
from .generated import system_pb2 as system
from .generated import keystore_pb2 as keystore
except ModuleNotFoundError:
print("Run `make py` to generate the protobuf messages")
sys.exit()
Expand Down Expand Up @@ -265,6 +267,15 @@ def set_app_static_privkey(self, privkey: bytes) -> None:
pass


@dataclass
class BitBoxConfig:
"""Configuration options"""

# If defined, this is called to enter the mnemonic passphrase on the host.
# It should return the passphrase, or None if the operation was cancelled.
enter_mnemonic_passphrase: Optional[Callable[[], Optional[str]]] = None


class BitBoxProtocol(ABC):
"""
Class for executing versioned BitBox operations
Expand Down Expand Up @@ -525,12 +536,13 @@ def cancel_outstanding_request(self) -> None:
class BitBoxCommonAPI:
"""Class to communicate with a BitBox device"""

# pylint: disable=too-many-public-methods,too-many-arguments
# pylint: disable=too-many-public-methods,too-many-arguments,too-many-branches
def __init__(
self,
transport: TransportLayer,
device_info: Optional[DeviceInfo],
noise_config: BitBoxNoiseConfig,
config: BitBoxConfig = BitBoxConfig(),
):
"""
Can raise LibraryVersionOutdatedException. check_min_version() should be called following
Expand Down Expand Up @@ -577,9 +589,13 @@ def __init__(

if self.version >= semver.VersionInfo(2, 0, 0):
noise_config.attestation_check(self._perform_attestation())
self._bitbox_protocol.unlock_query()
# Starting with v9.23, we can use the unlock function below after noise pairing.
if self.version < semver.VersionInfo(9, 23, 0):
self._bitbox_protocol.unlock_query()

self._bitbox_protocol.noise_connect(noise_config)
if self.version >= semver.VersionInfo(9, 23, 0):
self.unlock(config)

# pylint: disable=too-many-return-statements
def _perform_attestation(self) -> bool:
Expand Down Expand Up @@ -658,6 +674,47 @@ def _msg_query(
print(response)
return response

def unlock(
self,
config: BitBoxConfig,
) -> None:
"""
Prompt to unlock the device. If already unlocked, nothing happens. If
`config.enter_mnemonic_passphrase` is defined and the user chooses to enter the passphrase
on the host, this callback will be called to retrieve the passphrase.
"""
# pylint: disable=no-member
try:
request = hww.Request()
request.unlock.CopyFrom(
keystore.UnlockRequest(
supports_host_passphrase=config.enter_mnemonic_passphrase is not None,
)
)

while True:
response = self._msg_query(request)
response_type = response.WhichOneof("response")

if (
response_type == "unlock_host_info"
and response.unlock_host_info.type
== keystore.UnlockRequestHostInfoResponse.InfoType.PASSPHRASE
):
assert config.enter_mnemonic_passphrase is not None
request = hww.Request()
request.unlock_host_info.CopyFrom(
keystore.UnlockHostInfoRequest(
passphrase=config.enter_mnemonic_passphrase(),
)
)
elif response_type == "success":
break
else:
raise Exception("Unexpected response")
except OSError:
pass

def reboot(
self, purpose: "system.RebootRequest.Purpose.V" = system.RebootRequest.Purpose.UPGRADE
) -> bool:
Expand Down
8 changes: 4 additions & 4 deletions py/bitbox02/bitbox02/communication/generated/hww_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading