Skip to content

Commit 30e47f1

Browse files
committed
api: add Unlock call w/ support for entering passphrase on the host
Previously, the host would unlock before the noise channel was established using `OP_UNLOCK`, using a raw unencrypted call that is using protobufs. To support entering a passphrase on the host, we add a `UnlockRequest` protobuf message. This needs to happen after the noise channel was established because: - we want the passphrase to be encrypted in flight - we want to make use of protobuf and `next_request()` to query the host `UnlockRequest` has a flag `supports_host_passphrase`, so wallets (especially third party apps) that do not support this yet still work seamlessly, as without support the passphrase is simply entered on the device. If the host app supports it, then the device will ask the user if they want to enter on the device or on the host. The query to the host uses `UnlockRequestHostInfoResponse`. It contains the `type` enum for future compatbilitiy in case we want to add more unlock options in the future (e.g. fingerprints). The `workflow::unlock` calls now get a callback to pick the method of entering the passphrase, so the old `OP_UNLOCK` can use the previous way, while `UnlockRequest` provides a callback to ask the user where to enter and proceeds accordingly.
1 parent b51b95c commit 30e47f1

File tree

20 files changed

+421
-48
lines changed

20 files changed

+421
-48
lines changed

messages/hww.proto

+3
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@ message Request {
6767
ElectrumEncryptionKeyRequest electrum_encryption_key = 26;
6868
CardanoRequest cardano = 27;
6969
BIP85Request bip85 = 28;
70+
UnlockRequest unlock = 29;
71+
UnlockHostInfoRequest unlock_host_info = 30;
7072
}
7173
}
7274

@@ -89,5 +91,6 @@ message Response {
8991
ElectrumEncryptionKeyResponse electrum_encryption_key = 14;
9092
CardanoResponse cardano = 15;
9193
BIP85Response bip85 = 16;
94+
UnlockRequestHostInfoResponse unlock_host_info = 17;
9295
}
9396
}

messages/keystore.proto

+21
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,24 @@ message BIP85Response {
3131
bytes ln = 2;
3232
}
3333
}
34+
35+
message UnlockRequest {
36+
// If true, the device will be allowed to ask the user to enter the passphrase on the host. If
37+
// the user accepts, the host will receive a `UnlockRequestHostInfoResponse` with type
38+
// `PASSPHRASE`.
39+
bool supports_host_passphrase = 1;
40+
}
41+
42+
message UnlockRequestHostInfoResponse {
43+
enum InfoType {
44+
UNKNOWN = 0;
45+
// Respond with `UnlockHostInfoRequest` containing the passphrase.
46+
PASSPHRASE = 1;
47+
}
48+
InfoType type = 1;
49+
}
50+
51+
message UnlockHostInfoRequest {
52+
// Omit if type==PASSPHRASE and entering the passhrase is cancelled on the host.
53+
optional string passphrase = 1;
54+
}

py/bitbox02/bitbox02/communication/bitbox_api_protocol.py

+59-2
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"""BitBox02"""
1515

1616
from abc import ABC, abstractmethod
17+
from dataclasses import dataclass
1718
import os
1819
import enum
1920
import sys
@@ -36,6 +37,7 @@
3637
try:
3738
from .generated import hww_pb2 as hww
3839
from .generated import system_pb2 as system
40+
from .generated import keystore_pb2 as keystore
3941
except ModuleNotFoundError:
4042
print("Run `make py` to generate the protobuf messages")
4143
sys.exit()
@@ -265,6 +267,15 @@ def set_app_static_privkey(self, privkey: bytes) -> None:
265267
pass
266268

267269

270+
@dataclass
271+
class BitBoxConfig:
272+
"""Configuration options"""
273+
274+
# If defined, this is called to enter the mnemonic passphrase on the host.
275+
# It should return the passphrase, or None if the operation was cancelled.
276+
enter_mnemonic_passphrase: Optional[Callable[[], Optional[str]]] = None
277+
278+
268279
class BitBoxProtocol(ABC):
269280
"""
270281
Class for executing versioned BitBox operations
@@ -525,12 +536,13 @@ def cancel_outstanding_request(self) -> None:
525536
class BitBoxCommonAPI:
526537
"""Class to communicate with a BitBox device"""
527538

528-
# pylint: disable=too-many-public-methods,too-many-arguments
539+
# pylint: disable=too-many-public-methods,too-many-arguments,too-many-branches
529540
def __init__(
530541
self,
531542
transport: TransportLayer,
532543
device_info: Optional[DeviceInfo],
533544
noise_config: BitBoxNoiseConfig,
545+
config: BitBoxConfig = BitBoxConfig(),
534546
):
535547
"""
536548
Can raise LibraryVersionOutdatedException. check_min_version() should be called following
@@ -577,9 +589,13 @@ def __init__(
577589

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

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

584600
# pylint: disable=too-many-return-statements
585601
def _perform_attestation(self) -> bool:
@@ -658,6 +674,47 @@ def _msg_query(
658674
print(response)
659675
return response
660676

677+
def unlock(
678+
self,
679+
config: BitBoxConfig,
680+
) -> None:
681+
"""
682+
Prompt to unlock the device. If already unlocked, nothing happens. If
683+
`config.enter_mnemonic_passphrase` is defined and the user chooses to enter the passphrase
684+
on the host, this callback will be called to retrieve the passphrase.
685+
"""
686+
# pylint: disable=no-member
687+
try:
688+
request = hww.Request()
689+
request.unlock.CopyFrom(
690+
keystore.UnlockRequest(
691+
supports_host_passphrase=config.enter_mnemonic_passphrase is not None,
692+
)
693+
)
694+
695+
while True:
696+
response = self._msg_query(request)
697+
response_type = response.WhichOneof("response")
698+
699+
if (
700+
response_type == "unlock_host_info"
701+
and response.unlock_host_info.type
702+
== keystore.UnlockRequestHostInfoResponse.InfoType.PASSPHRASE
703+
):
704+
assert config.enter_mnemonic_passphrase is not None
705+
request = hww.Request()
706+
request.unlock_host_info.CopyFrom(
707+
keystore.UnlockHostInfoRequest(
708+
passphrase=config.enter_mnemonic_passphrase(),
709+
)
710+
)
711+
elif response_type == "success":
712+
break
713+
else:
714+
raise Exception("Unexpected response")
715+
except OSError:
716+
pass
717+
661718
def reboot(
662719
self, purpose: "system.RebootRequest.Purpose.V" = system.RebootRequest.Purpose.UPGRADE
663720
) -> bool:

py/bitbox02/bitbox02/communication/generated/hww_pb2.py

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)