Skip to content

Commit

Permalink
api: add a custom BIP85 derivation for Lightning hot wallets
Browse files Browse the repository at this point in the history
For the generation of a mnemonic for Breez-SDK Lightning hot wallets
in the BitBoxApp.

Using the regular BIP85-BIP39 path like `m/83696968'/39'/0'/12'/0'`
for this is problematic as a user might use the same mnemonics for
something else, e.g. another cold storage wallet. In such a case, the
derived mnemonic of the user would become hot without the user
realizing it.

For the purpose of generating a mnemonic for a lightning hot wallet
using BIP85, we should instead use a different application number
dedicated to this.

We are going with number `19534'`, which is unlikely to interfere with
other uses. It's hex representation `0x4c4e` spells `LN` in ASCII.

The BIP85 derivation would then be
`m/83696968'/{app_no}/{language}'/{words}'/` as in BIP85-BIP39, but
use `19534'` for the `app_no` instead of `39'`, so:

    m/83696968'/19534'/0'/12'/0'

We restrict to only 12 words for LN hot wallet mnemonics for
simplicity and easier recovery for users.

The index represents the account number - for now we only allow
`0` (the first account), and can extend to multiple if there is ever a
need.
  • Loading branch information
benma committed Feb 19, 2024
1 parent f642d65 commit 6d5239f
Show file tree
Hide file tree
Showing 13 changed files with 205 additions and 26 deletions.
6 changes: 6 additions & 0 deletions messages/keystore.proto
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +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;
string ln = 2;
}
}
23 changes: 22 additions & 1 deletion py/bitbox02/bitbox02/bitbox02/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -690,7 +690,28 @@ def bip85_39(self) -> None:
bip39=google.protobuf.empty_pb2.Empty(),
)
)
self._msg_query(request)
response = self._msg_query(request, expected_response="bip85").bip85
assert response.WhichOneof("app") == "bip39"

def bip85_ln(self) -> str:
"""
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
12 changes: 7 additions & 5 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.

29 changes: 23 additions & 6 deletions py/bitbox02/bitbox02/communication/generated/keystore_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -37,28 +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"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39"]) -> None: ...
def WhichOneof(self, oneof_group: typing_extensions.Literal["app",b"app"]) -> typing.Optional[typing_extensions.Literal["bip39"]]: ...
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: typing.Text
def __init__(self,
*,
bip39: typing.Optional[google.protobuf.empty_pb2.Empty] = ...,
ln: typing.Text = ...,
) -> None: ...
def HasField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39"]) -> builtins.bool: ...
def ClearField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39"]) -> None: ...
def WhichOneof(self, oneof_group: typing_extensions.Literal["app",b"app"]) -> typing.Optional[typing_extensions.Literal["bip39"]]: ...
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
13 changes: 12 additions & 1 deletion py/send_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,17 @@ def _get_electrum_encryption_key(self) -> None:
)

def _bip85_39(self) -> None:
self._device.bip85_39()
try:
self._device.bip85_39()
except UserAbortException:
print("Aborted by user")

def _bip85_ln(self) -> None:
try:
mnemonic = self._device.bip85_ln()
print("Lightning hot wallet mnemonic:", mnemonic)
except UserAbortException:
print("Aborted by user")

def _btc_address(self) -> None:
def address(display: bool) -> str:
Expand Down Expand Up @@ -1392,6 +1402,7 @@ def _menu_init(self) -> None:
("Cardano", self._cardano),
("Show Electrum wallet encryption key", self._get_electrum_encryption_key),
("BIP85 - BIP39", self._bip85_39),
("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
33 changes: 33 additions & 0 deletions src/keystore.c
Original file line number Diff line number Diff line change
Expand Up @@ -864,6 +864,39 @@ bool keystore_bip85_bip39(
return snprintf_result >= 0 && snprintf_result < (int)mnemonic_out_size;
}

bool keystore_bip85_ln(uint32_t index, char* mnemonic_out, size_t mnemonic_out_size)
{
uint32_t words = 12;
size_t seed_size = 16;

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,
words + 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;
}

char* mnemonic = NULL;
if (bip39_mnemonic_from_bytes(NULL, entropy, seed_size, &mnemonic) != WALLY_OK) {
return false;
}
int snprintf_result = snprintf(mnemonic_out, mnemonic_out_size, "%s", mnemonic);
util_cleanup_str(&mnemonic);
free(mnemonic);
return snprintf_result >= 0 && snprintf_result < (int)mnemonic_out_size;
}

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

/**
* Computes a 12 word BIP39 mnemonic 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 12 word
* mnemonics.
* @param[in] index must be smaller than `BIP32_INITIAL_HARDENED_CHILD`.
* @param[out] mnemonic_out resulting mnemonic
* @param[in] mnemonic_out_size size of mnemonic_out. Should be at least 216 bytes (longest possible
* 24 word phrase plus null terminator).
*/
USE_RESULT bool keystore_bip85_ln(uint32_t index, char* mnemonic_out, size_t mnemonic_out_size);

/**
* 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
43 changes: 37 additions & 6 deletions src/rust/bitbox02-rust/src/hww/api/bip85.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,26 +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;
use alloc::string::String;

/// Processes a BIP-85 API call.
pub async fn process(request: &pb::Bip85Request) -> Result<Response, Error> {
match request.app {
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.
#[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 @@ -109,10 +118,32 @@ async fn process_bip39() -> Result<(), Error> {
.await?;

let mnemonic = keystore::bip85_bip39(num_words, index)?;
let words: Vec<&str> = mnemonic.split(' ').collect();
let words: alloc::vec::Vec<&str> = mnemonic.split(' ').collect();
mnemonic::show_and_confirm_mnemonic(&words).await?;

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

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<String, 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)
}
14 changes: 12 additions & 2 deletions src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1513,22 +1513,30 @@ pub struct ElectrumEncryptionKeyResponse {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bip85Request {
#[prost(oneof = "bip85_request::App", tags = "1")]
#[prost(oneof = "bip85_request::App", tags = "1, 2")]
pub app: ::core::option::Option<bip85_request::App>,
}
/// Nested message and enum types in `BIP85Request`.
pub mod bip85_request {
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct AppLn {
#[prost(uint32, tag = "1")]
pub account_number: u32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Oneof)]
pub enum App {
#[prost(message, tag = "1")]
Bip39(()),
#[prost(message, tag = "2")]
Ln(AppLn),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct Bip85Response {
#[prost(oneof = "bip85_response::App", tags = "1")]
#[prost(oneof = "bip85_response::App", tags = "1, 2")]
pub app: ::core::option::Option<bip85_response::App>,
}
/// Nested message and enum types in `BIP85Response`.
Expand All @@ -1538,6 +1546,8 @@ pub mod bip85_response {
pub enum App {
#[prost(message, tag = "1")]
Bip39(()),
#[prost(string, tag = "2")]
Ln(::prost::alloc::string::String),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
Expand Down
Loading

0 comments on commit 6d5239f

Please sign in to comment.