Skip to content

Commit

Permalink
Merge commit 'c7e091ce'
Browse files Browse the repository at this point in the history
  • Loading branch information
benma committed Feb 20, 2024
2 parents b7735aa + c7e091c commit 4d2434f
Show file tree
Hide file tree
Showing 15 changed files with 264 additions and 31 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
### [Unreleased]
- Add support for deriving BIP-39 mnemonics according to BIP-85

### 9.17.0
- Add support for deriving mnemonics for Lightning hot wallets according to BIP-85 (using a custom
BIP-85 application number)

### 9.16.0
- Disable screensaver when displaying a receive address, confirming a transaction, and other interactive actions
- Add Sepolia testnet for Ethereum
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ endif()
#
# Versions MUST contain three parts and start with lowercase 'v'.
# Example 'v1.0.0'. They MUST not contain a pre-release label such as '-beta'.
set(FIRMWARE_VERSION "v9.16.0")
set(FIRMWARE_BTC_ONLY_VERSION "v9.16.0")
set(FIRMWARE_VERSION "v9.17.0")
set(FIRMWARE_BTC_ONLY_VERSION "v9.17.0")
set(BOOTLOADER_VERSION "v1.0.5")

find_package(PythonInterp 3.6 REQUIRED)
Expand Down
14 changes: 14 additions & 0 deletions messages/keystore.proto
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
syntax = "proto3";
package shiftcrypto.bitbox02;

import "google/protobuf/empty.proto";

message ElectrumEncryptionKeyRequest {
repeated uint32 keypath = 1;
}
Expand All @@ -13,7 +15,19 @@ message ElectrumEncryptionKeyResponse {
}

message BIP85Request {
message AppLn {
uint32 account_number = 1;
}

oneof app {
google.protobuf.Empty bip39 = 1;
AppLn ln = 2;
}
}

message BIP85Response {
oneof app {
google.protobuf.Empty bip39 = 1;
bytes ln = 2;
}
}
36 changes: 31 additions & 5 deletions py/bitbox02/bitbox02/bitbox02/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from bitbox02.communication.generated import common_pb2 as common
from bitbox02.communication.generated import keystore_pb2 as keystore
from bitbox02.communication.generated import antiklepto_pb2 as antiklepto
import google.protobuf.empty_pb2

# pylint: disable=unused-import
# We export it in __init__.py
Expand Down Expand Up @@ -678,14 +679,39 @@ def electrum_encryption_key(self, keypath: Sequence[int]) -> str:
)
return self._msg_query(request).electrum_encryption_key.key

def bip85(self) -> None:
"""Invokes the BIP-85 workflow on the device"""
self._require_atleast(semver.VersionInfo(9, 16, 0))
def bip85_bip39(self) -> None:
"""Invokes the BIP85-BIP39 workflow on the device"""
self._require_atleast(semver.VersionInfo(9, 17, 0))

# pylint: disable=no-member
request = hww.Request()
request.bip85.CopyFrom(keystore.BIP85Request())
self._msg_query(request)
request.bip85.CopyFrom(
keystore.BIP85Request(
bip39=google.protobuf.empty_pb2.Empty(),
)
)
response = self._msg_query(request, expected_response="bip85").bip85
assert response.WhichOneof("app") == "bip39"

def bip85_ln(self) -> bytes:
"""
Generates and returns a mnemonic for a hot Lightning wallet from the device using BIP-85.
"""
self._require_atleast(semver.VersionInfo(9, 17, 0))

# Only account_number=0 is allowed for now.
account_number = 0

# pylint: disable=no-member
request = hww.Request()
request.bip85.CopyFrom(
keystore.BIP85Request(
ln=keystore.BIP85Request.AppLn(account_number=account_number),
)
)
response = self._msg_query(request, expected_response="bip85").bip85
assert response.WhichOneof("app") == "ln"
return response.ln

def enable_mnemonic_passphrase(self) -> None:
"""
Expand Down
21 changes: 12 additions & 9 deletions py/bitbox02/bitbox02/communication/generated/keystore_pb2.py

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

34 changes: 34 additions & 0 deletions py/bitbox02/bitbox02/communication/generated/keystore_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ isort:skip_file
"""
import builtins
import google.protobuf.descriptor
import google.protobuf.empty_pb2
import google.protobuf.internal.containers
import google.protobuf.message
import typing
Expand Down Expand Up @@ -36,12 +37,45 @@ global___ElectrumEncryptionKeyResponse = ElectrumEncryptionKeyResponse

class BIP85Request(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
class AppLn(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
ACCOUNT_NUMBER_FIELD_NUMBER: builtins.int
account_number: builtins.int
def __init__(self,
*,
account_number: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["account_number",b"account_number"]) -> None: ...

BIP39_FIELD_NUMBER: builtins.int
LN_FIELD_NUMBER: builtins.int
@property
def bip39(self) -> google.protobuf.empty_pb2.Empty: ...
@property
def ln(self) -> global___BIP85Request.AppLn: ...
def __init__(self,
*,
bip39: typing.Optional[google.protobuf.empty_pb2.Empty] = ...,
ln: typing.Optional[global___BIP85Request.AppLn] = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39","ln",b"ln"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39","ln",b"ln"]) -> None: ...
def WhichOneof(self, oneof_group: typing_extensions.Literal["app",b"app"]) -> typing.Optional[typing_extensions.Literal["bip39","ln"]]: ...
global___BIP85Request = BIP85Request

class BIP85Response(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
BIP39_FIELD_NUMBER: builtins.int
LN_FIELD_NUMBER: builtins.int
@property
def bip39(self) -> google.protobuf.empty_pb2.Empty: ...
ln: builtins.bytes
def __init__(self,
*,
bip39: typing.Optional[google.protobuf.empty_pb2.Empty] = ...,
ln: builtins.bytes = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39","ln",b"ln"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39","ln",b"ln"]) -> None: ...
def WhichOneof(self, oneof_group: typing_extensions.Literal["app",b"app"]) -> typing.Optional[typing_extensions.Literal["bip39","ln"]]: ...
global___BIP85Response = BIP85Response
17 changes: 14 additions & 3 deletions py/send_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,8 +340,18 @@ def _get_electrum_encryption_key(self) -> None:
),
)

def _bip85(self) -> None:
self._device.bip85()
def _bip85_bip39(self) -> None:
try:
self._device.bip85_bip39()
except UserAbortException:
print("Aborted by user")

def _bip85_ln(self) -> None:
try:
entropy = self._device.bip85_ln()
print("Derived entropy for a Breez Lightning wallet:", entropy.hex())
except UserAbortException:
print("Aborted by user")

def _btc_address(self) -> None:
def address(display: bool) -> str:
Expand Down Expand Up @@ -1391,7 +1401,8 @@ def _menu_init(self) -> None:
("Sign Ethereum Typed Message (EIP-712)", self._sign_eth_typed_message),
("Cardano", self._cardano),
("Show Electrum wallet encryption key", self._get_electrum_encryption_key),
("BIP85", self._bip85),
("BIP85 - BIP39", self._bip85_bip39),
("BIP85 - LN", self._bip85_ln),
("Reset Device", self._reset_device),
)
choice = ask_user(choices)
Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ add_custom_target(rust-bindgen
--allowlist-function keystore_get_bip39_word
--allowlist-function keystore_get_ed25519_seed
--allowlist-function keystore_bip85_bip39
--allowlist-function keystore_bip85_ln
--allowlist-function keystore_secp256k1_compressed_to_uncompressed
--allowlist-function keystore_secp256k1_nonce_commit
--allowlist-function keystore_secp256k1_sign
Expand Down
24 changes: 24 additions & 0 deletions src/keystore.c
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,30 @@ bool keystore_bip85_bip39(
return snprintf_result >= 0 && snprintf_result < (int)mnemonic_out_size;
}

bool keystore_bip85_ln(uint32_t index, uint8_t* entropy_out)
{
if (index >= BIP32_INITIAL_HARDENED_CHILD) {
return false;
}

const uint32_t keypath[] = {
83696968 + BIP32_INITIAL_HARDENED_CHILD,
19534 + BIP32_INITIAL_HARDENED_CHILD,
0 + BIP32_INITIAL_HARDENED_CHILD,
12 + BIP32_INITIAL_HARDENED_CHILD,
index + BIP32_INITIAL_HARDENED_CHILD,
};

uint8_t entropy[64] = {0};
UTIL_CLEANUP_64(entropy);
if (!_bip85_entropy(keypath, sizeof(keypath) / sizeof(uint32_t), entropy)) {
return false;
}

memcpy(entropy_out, entropy, 16);
return true;
}

USE_RESULT bool keystore_encode_xpub_at_keypath(
const uint32_t* keypath,
size_t keypath_len,
Expand Down
10 changes: 10 additions & 0 deletions src/keystore.h
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,16 @@ USE_RESULT bool keystore_bip85_bip39(
char* mnemonic_out,
size_t mnemonic_out_size);

/**
* Computes a 16 byte deterministic seed specifically for Lightning hot wallets according to BIP-85.
* It is the same as BIP-85 with app number 39', but instead using app number 19534' (= 0x4c4e =
* 'LN'). https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#bip39
* Restricted to 16 byte output entropy.
* @param[in] index must be smaller than `BIP32_INITIAL_HARDENED_CHILD`.
* @param[out] entropy_out resulting entropy, must be at least 16 bytes in size.
*/
USE_RESULT bool keystore_bip85_ln(uint32_t index, uint8_t* entropy_out);

/**
* Encode an xpub at the given `keypath` as 78 bytes according to BIP32. The version bytes are
* the ones corresponding to `xpub`, i.e. 0x0488B21E.
Expand Down
2 changes: 1 addition & 1 deletion src/rust/bitbox02-rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ app-cardano = [
"ed25519"
]

app-bip85 = []
app-bip85-bip39 = []

testing = [
# enable these deps
Expand Down
4 changes: 0 additions & 4 deletions src/rust/bitbox02-rust/src/hww/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ pub mod bitcoin;
mod cardano;

mod backup;
#[cfg(feature = "app-bip85")]
mod bip85;
mod device_info;
mod electrum;
Expand Down Expand Up @@ -184,9 +183,6 @@ async fn process_api(request: &Request) -> Result<Response, Error> {
.map(|r| Response::Cardano(pb::CardanoResponse { response: Some(r) })),
#[cfg(not(feature = "app-cardano"))]
Request::Cardano(_) => Err(Error::Disabled),
#[cfg(not(feature = "app-bip85"))]
Request::Bip85(_) => Err(Error::Disabled),
#[cfg(feature = "app-bip85")]
Request::Bip85(ref request) => bip85::process(request).await,
_ => Err(Error::InvalidInput),
}
Expand Down
51 changes: 46 additions & 5 deletions src/rust/bitbox02-rust/src/hww/api/bip85.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,35 @@ use super::Error;

use pb::response::Response;

use bitbox02::keystore;
use crate::workflow::confirm;

use crate::workflow::trinary_choice::{choose, TrinaryChoice};
use crate::workflow::{confirm, menu, mnemonic, status, trinary_input_string};
use bitbox02::keystore;

use alloc::vec::Vec;

/// Processes a BIP-85 API call.
pub async fn process(request: &pb::Bip85Request) -> Result<Response, Error> {
match &request.app {
None => Err(Error::InvalidInput),
#[cfg(not(feature = "app-bip85-bip39"))]
Some(pb::bip85_request::App::Bip39(())) => Err(Error::Disabled),
#[cfg(feature = "app-bip85-bip39")]
Some(pb::bip85_request::App::Bip39(())) => Ok(Response::Bip85(pb::Bip85Response {
app: Some(pb::bip85_response::App::Bip39(process_bip39().await?)),
})),
Some(pb::bip85_request::App::Ln(request)) => Ok(Response::Bip85(pb::Bip85Response {
app: Some(pb::bip85_response::App::Ln(process_ln(request).await?)),
})),
}
}

/// Derives and displays a BIP-39 seed according to BIP-85:
/// https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#bip39.
pub async fn process(pb::Bip85Request {}: &pb::Bip85Request) -> Result<Response, Error> {
#[cfg(feature = "app-bip85-bip39")]
async fn process_bip39() -> Result<(), Error> {
use crate::workflow::trinary_choice::{choose, TrinaryChoice};
use crate::workflow::{menu, mnemonic, status, trinary_input_string};

confirm::confirm(&confirm::Params {
title: "BIP-85",
body: "Derive BIP-39\nmnemonic?",
Expand Down Expand Up @@ -104,5 +123,27 @@ pub async fn process(pb::Bip85Request {}: &pb::Bip85Request) -> Result<Response,

status::status("Finished", true).await;

Ok(Response::Bip85(pb::Bip85Response {}))
Ok(())
}

/// Derives and displays a LN seed according to BIP-85.
/// It is the same as BIP-85 with app number 39', but instead using app number 19534' (= 0x4c4e = 'LN'),
/// and restricted to 12 word mnemonics.
/// https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#bip39
async fn process_ln(
&pb::bip85_request::AppLn { account_number }: &pb::bip85_request::AppLn,
) -> Result<Vec<u8>, Error> {
// We allow only one LN account until we see a reason to have more.
if account_number != 0 {
return Err(Error::InvalidInput);
}
confirm::confirm(&confirm::Params {
title: "",
body: "Create\nLightning wallet\non host device?",
longtouch: true,
..Default::default()
})
.await?;

keystore::bip85_ln(account_number).map_err(|_| Error::Generic)
}
Loading

0 comments on commit 4d2434f

Please sign in to comment.