Skip to content

Commit

Permalink
wip: experiments restoring
Browse files Browse the repository at this point in the history
restore kind of works: tested with 12 and 24 words seed.
  • Loading branch information
Beerosagos committed Dec 17, 2024
1 parent 10c4bd0 commit 1cbdd50
Show file tree
Hide file tree
Showing 15 changed files with 274 additions and 62 deletions.
2 changes: 2 additions & 0 deletions messages/shamir.proto
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ package shiftcrypto.bitbox02;
message ShowShamirRequest {
}
message RestoreFromShamirRequest {
uint32 timestamp = 1;
int32 timezone_offset = 2;
}
13 changes: 13 additions & 0 deletions py/bitbox02/bitbox02/bitbox02/bitbox02.py
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,19 @@ def restore_from_mnemonic(self) -> None:
)
self._msg_query(request)

def restore_from_shamir(self) -> None:
"""
Restore from shamir backup. Raises a Bitbox02Exception on failure.
"""
request = hww.Request()
# pylint: disable=no-member
request.restore_from_shamir.CopyFrom(
shamir.RestoreFromShamirRequest(
timestamp=int(time.time()), timezone_offset=time.localtime().tm_gmtoff
)
)
self._msg_query(request)

def _cardano_msg_query(
self, cardano_request: cardano.CardanoRequest, expected_response: Optional[str] = None
) -> cardano.CardanoResponse:
Expand Down
4 changes: 2 additions & 2 deletions py/bitbox02/bitbox02/communication/generated/shamir_pb2.py

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

10 changes: 10 additions & 0 deletions py/bitbox02/bitbox02/communication/generated/shamir_pb2.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
@generated by mypy-protobuf. Do not edit manually!
isort:skip_file
"""
import builtins
import google.protobuf.descriptor
import google.protobuf.message
import typing_extensions

DESCRIPTOR: google.protobuf.descriptor.FileDescriptor

Expand All @@ -15,6 +17,14 @@ global___ShowShamirRequest = ShowShamirRequest

class RestoreFromShamirRequest(google.protobuf.message.Message):
DESCRIPTOR: google.protobuf.descriptor.Descriptor
TIMESTAMP_FIELD_NUMBER: builtins.int
TIMEZONE_OFFSET_FIELD_NUMBER: builtins.int
timestamp: builtins.int
timezone_offset: builtins.int
def __init__(self,
*,
timestamp: builtins.int = ...,
timezone_offset: builtins.int = ...,
) -> None: ...
def ClearField(self, field_name: typing_extensions.Literal["timestamp",b"timestamp","timezone_offset",b"timezone_offset"]) -> None: ...
global___RestoreFromShamirRequest = RestoreFromShamirRequest
8 changes: 8 additions & 0 deletions py/send_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,13 @@ def _restore_from_mnemonic(self) -> None:
except UserAbortException:
print("Aborted by user")

def _restore_from_shamir(self) -> None:
try:
self._device.restore_from_shamir()
print("Restore successful")
except UserAbortException:
print("Aborted by user")

def _list_device_info(self) -> None:
print(f"All info: {self._device.device_info()}")

Expand Down Expand Up @@ -1397,6 +1404,7 @@ def _menu_notinit(self) -> None:
("Set up a new wallet", self._setup_workflow),
("Restore from backup", self._restore_backup_workflow),
("Restore from mnemonic", self._restore_from_mnemonic),
("Restore from shamir", self._restore_from_shamir),
("List device info", self._list_device_info),
("Reboot into bootloader", self._reboot),
("Check if SD card inserted", self._check_sd_presence),
Expand Down
11 changes: 8 additions & 3 deletions src/keystore.c
Original file line number Diff line number Diff line change
Expand Up @@ -570,9 +570,9 @@ bool keystore_get_bip39_mnemonic_from_bytes(const uint8_t* bytes, size_t len, ch
return false;
}

if (len > KEYSTORE_MAX_SEED_LENGTH) {
return false;
}
/* if (len > KEYSTORE_MAX_SEED_LENGTH) { */
/* return false; */
/* } */
char* mnemonic = NULL;
if (bip39_mnemonic_from_bytes(NULL, bytes, len, &mnemonic) != WALLY_OK) {
return false;
Expand All @@ -590,6 +590,11 @@ bool keystore_bip39_mnemonic_to_seed(const char* mnemonic, uint8_t* seed_out, si
return bip39_mnemonic_to_bytes(NULL, mnemonic, seed_out, 32, seed_len_out) == WALLY_OK;
}

bool keystore_bip39_mnemonic_to_bytes(const char* mnemonic, uint8_t* bytes_out, size_t bytes_len, size_t* bytes_len_out)
{
return bip39_mnemonic_to_bytes(NULL, mnemonic, bytes_out, bytes_len, bytes_len_out) == WALLY_OK;
}

static bool _get_xprv(const uint32_t* keypath, const size_t keypath_len, struct ext_key* xprv_out)
{
if (keystore_is_locked()) {
Expand Down
9 changes: 9 additions & 0 deletions src/keystore.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,15 @@ USE_RESULT bool keystore_bip39_mnemonic_to_seed(
uint8_t* seed_out,
size_t* seed_len_out);

/**
* Turn a bip39 mnemonic into a byte array. Make sure to use UTIL_CLEANUP_32 to destroy it.
* @param[in] mnemonic 12/18/24 word bip39 mnemonic
* @param[in] bytes_len size of the bytes array
* @param[out] bytes_out
* @param[out] bytes_len_out will be the size of the seed
*/
USE_RESULT bool keystore_bip39_mnemonic_to_bytes(const char* mnemonic, uint8_t* bytes_out, size_t bytes_len, size_t* bytes_len_out);

/**
* Can be used only if the keystore is unlocked. Returns the derived xpub,
* using bip32 derivation. Derivation is done from the xprv master, so hardened
Expand Down
2 changes: 2 additions & 0 deletions src/rust/bitbox02-rust/src/hww/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ fn can_call(request: &Request) -> bool {
Request::SetPassword(_) => matches!(state, State::Uninitialized | State::Seeded),
Request::RestoreBackup(_) => matches!(state, State::Uninitialized | State::Seeded),
Request::RestoreFromMnemonic(_) => matches!(state, State::Uninitialized | State::Seeded),
Request::RestoreFromShamir(_) => matches!(state, State::Uninitialized | State::Seeded),
Request::CreateBackup(_) => matches!(state, State::Seeded | State::Initialized),
Request::ShowMnemonic(_) => matches!(state, State::Seeded | State::Initialized),
Request::ShowShamir(_) => matches!(state, State::Seeded | State::Initialized),
Expand Down Expand Up @@ -163,6 +164,7 @@ async fn process_api(request: &Request) -> Result<Response, Error> {
Request::RestoreFromMnemonic(ref request) => restore::from_mnemonic(request).await,
Request::ElectrumEncryptionKey(ref request) => electrum::process(request).await,
Request::ShowShamir(_) => show_shamir::process().await,
Request::RestoreFromShamir(ref request) => restore::from_shamir(request).await,

#[cfg(feature = "app-ethereum")]
Request::Eth(pb::EthRequest {
Expand Down
93 changes: 93 additions & 0 deletions src/rust/bitbox02-rust/src/hww/api/restore.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

use super::Error;
use crate::pb;
use alloc::vec::Vec;

use pb::response::Response;

Expand Down Expand Up @@ -150,3 +151,95 @@ pub async fn from_mnemonic(
unlock::unlock_bip39().await;
Ok(Response::Success(pb::Success {}))
}

pub async fn from_shamir(
#[cfg_attr(not(feature = "app-u2f"), allow(unused_variables))] &pb::RestoreFromShamirRequest {
timestamp,
timezone_offset,
}: &pb::RestoreFromShamirRequest,
) -> Result<Response, Error> {
#[cfg(feature = "app-u2f")]
{
let datetime_string = bitbox02::format_datetime(timestamp, timezone_offset, false)
.map_err(|_| Error::InvalidInput)?;
confirm::confirm(&confirm::Params {
title: "Is now?",
body: &datetime_string,
accept_is_nextarrow: true,
..Default::default()
})
.await?;
}

let mnemonics = mnemonic::get_shamir().await?;
let mut shares: Vec<sharks::Share> = Vec::new();
for mnemonic in mnemonics {
let bytes = match bitbox02::keystore::bip39_mnemonic_to_bytes(&mnemonic, 36) {
Ok(bytes) => bytes,
Err(()) => {
status::status("Recovery words\ninvalid", false).await;
return Err(Error::Generic);
}
};
let s = sharks::Share::try_from(&bytes[3..]).unwrap();
shares.push(s);
// let share = Vec::from(&s);
// bitbox02::print_stdout(&format!("share: {}, len: {}\n", hex::encode(share.clone()), share.len()));
}

let sharks = sharks::Sharks(2);
let seed = match sharks.recover(shares.as_slice()) {
Ok(seed) => seed,
Err(_) => {
status::status("Recovery words\ninvalid", false).await;
// bitbox02::print_stdout(format!("Err: {}\n", err_str).as_str());
return Err(Error::Generic);
}
};

status::status("Recovery words\nvalid", true).await;

// bitbox02::print_stdout(&format!(
// "seed: {}, len: {}\n",
// hex::encode(seed.clone()),
// seed.len()
// ));
// let mnemonic_sentence = bitbox02::keystore::get_bip39_mnemonic_from_bytes(seed.as_ptr(), seed.len())?;
// bitbox02::print_stdout(format!("mnemonic: {}\n", mnemonic_sentence.as_str()).as_str());

// If entering password fails (repeat password does not match the first), we don't want to abort
// the process immediately. We break out only if the user confirms.
let password = loop {
match password::enter_twice().await {
Err(password::EnterTwiceError::DoNotMatch) => {
confirm::confirm(&confirm::Params {
title: "",
body: "Passwords\ndo not match.\nTry again?",
..Default::default()
})
.await?;
}
Err(password::EnterTwiceError::Cancelled) => return Err(Error::UserAbort),
Ok(password) => break password,
}
};

if let Err(err) = bitbox02::keystore::encrypt_and_store_seed(seed.as_slice(), &password) {
status::status(&format!("Could not\nrestore backup\n{:?}", err), false).await;
return Err(Error::Generic);
};

#[cfg(feature = "app-u2f")]
{
// Ignore error - the U2f counter not being set can lead to problems with U2F, but it should
// not fail the recovery, so the user can access their coins.
let _ = bitbox02::securechip::u2f_counter_set(timestamp);
}

bitbox02::memory::set_initialized().or(Err(Error::Memory))?;

// This should never fail.
bitbox02::keystore::unlock(&password).expect("restore_from_mnemonic: unlock failed");
unlock::unlock_bip39().await;
Ok(Response::Success(pb::Success {}))
}
70 changes: 32 additions & 38 deletions src/rust/bitbox02-rust/src/hww/api/show_shamir.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2020 Shift Crypto AG
// Copyright 2024 Shift Crypto AG
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -22,45 +22,29 @@ use pb::response::Response;

use crate::workflow::{mnemonic, status, unlock};
use bitbox02::keystore;
use sharks::{ Sharks, Share };
use rand_chacha::rand_core::SeedableRng;
use sharks::{Share, Sharks};

/// Handle the ShowShamir API call. This shows the seed shards encoded as
/// 12/18/24 BIP39 English words. Afterwards, for each word, the user
/// 15/27 BIP39 English words. Afterwards, for each word, the user
/// is asked to pick the right word among 5 words, to check if they
/// wrote it down correctly.
pub async fn process() -> Result<Response, Error> {
if bitbox02::memory::is_initialized() {
unlock::unlock_keystore("Unlock device", unlock::CanCancel::Yes).await?;
}
// Set a minimum threshold of 10 shares
let sharks = Sharks(3);

// // Obtain an iterator over the shares for secret [1, 2, 3, 4]
// // TODO: use RNG from SE?
let mut rng = rand_chacha::ChaCha8Rng::from_seed([0x90; 32]);
// Set a minimum threshold of 2 shares
const SHARES_THRESHOLD: u8 = 2;
const SHARES_MAX: usize = 3;
let sharks = Sharks(SHARES_THRESHOLD);

let mut seed = [0u8; 32];
// FIXME: this makes rand each shard generation. Should we use factory rand instead?
bitbox02::random::mcu_32_bytes(&mut seed);
let mut rng = rand_chacha::ChaCha8Rng::from_seed(seed);
let seed = bitbox02::keystore::copy_seed()?;
let dealer = sharks.dealer_rng(&seed, &mut rng);
// let dealer = sharks.dealer_rng(&[1,2,3,4], &mut rng);
// Get 3 shares
let mut shares: Vec<Share> = dealer.take(3).collect();
for s in shares {

// shares.remove(1);
// shares.remove(0);
// Recover the original secret!
// bitbox02::print_stdout("Recovering...\n");
// let secret = sharks.recover(shares.as_slice());
// match secret {
// Err(e) => bitbox02::print_stdout(&format!("Error {}\n", e)),
// Ok(_) => bitbox02::print_stdout("***test ok\n"),
// }
// assert_eq!(*secret.unwrap(), *seed);
let mnemonic_sentence = keystore::get_bip39_mnemonic_from_bytes(Vec::from(&s).as_ptr(), seed.len())?;

// let mnemonic_sentence = keystore::get_bip39_mnemonic()?;

// bitbox02::print_stdout(&format!("seed: {}, len: {}\n", hex::encode(seed.clone()), seed.len()));
confirm::confirm(&confirm::Params {
title: "Warning",
body: "DO NOT share your\nrecovery words with\nanyone!",
Expand All @@ -69,19 +53,29 @@ pub async fn process() -> Result<Response, Error> {
})
.await?;

confirm::confirm(&confirm::Params {
title: "Recovery\nwords",
body: "Please write down\nthe following words",
accept_is_nextarrow: true,
..Default::default()
})
.await?;

let words: Vec<&str> = mnemonic_sentence.split(' ').collect();
// Get 3 shares
let shares: Vec<Share> = dealer.take(SHARES_MAX).collect();
for (i, s) in shares.iter().enumerate() {
let share_slice = Vec::from(s);
// Sharks add a single byte to enumerate the shard. We add three bytes in front of it to
// get an additional 4 bytes to the seed and be compliant with BIP39.
let mut share_extended = vec![0, 0, 0];
share_extended.extend_from_slice(&share_slice);
// bitbox02::print_stdout(&format!("Share: {}, len: {}\n", hex::encode(share_extended.clone()), share_extended.len()));
let mnemonic_sentence = keystore::get_bip39_mnemonic_from_bytes(share_extended)?;

mnemonic::show_and_confirm_mnemonic(&words).await?;
confirm::confirm(&confirm::Params {
title: &format!("Recovery\nwords {}/{}", i + 1, SHARES_MAX),
body: "Please write down\nthe following words",
accept_is_nextarrow: true,
..Default::default()
})
.await?;

let words: Vec<&str> = mnemonic_sentence.split(' ').collect();
mnemonic::show_and_confirm_mnemonic(&words).await?;
}

bitbox02::memory::set_initialized().or(Err(Error::Memory))?;

status::status("Backup created", true).await;
Expand Down
11 changes: 9 additions & 2 deletions src/rust/bitbox02-rust/src/shiftcrypto.bitbox02.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1627,7 +1627,12 @@ pub struct SetMnemonicPassphraseEnabledRequest {
pub struct ShowShamirRequest {}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct RestoreFromShamirRequest {}
pub struct RestoreFromShamirRequest {
#[prost(uint32, tag = "1")]
pub timestamp: u32,
#[prost(int32, tag = "2")]
pub timezone_offset: i32,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct RebootRequest {
Expand Down Expand Up @@ -1702,7 +1707,7 @@ pub struct Success {}
pub struct Request {
#[prost(
oneof = "request::Request",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29"
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 24, 25, 26, 27, 28, 29, 30"
)]
pub request: ::core::option::Option<request::Request>,
}
Expand Down Expand Up @@ -1767,6 +1772,8 @@ pub mod request {
Bip85(super::Bip85Request),
#[prost(message, tag = "29")]
ShowShamir(super::ShowShamirRequest),
#[prost(message, tag = "30")]
RestoreFromShamir(super::RestoreFromShamirRequest),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
Expand Down
Loading

0 comments on commit 1cbdd50

Please sign in to comment.