Skip to content
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

BIP85 derived entropy for Breez SDK #1179

Merged
merged 2 commits into from
Feb 20, 2024
Merged
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
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 @@ -88,8 +88,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
Loading