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

wip: shamir secret sharing #1343

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
wip: experiments restoring
restore kind of works: tested with 12 and 24 words seed.
  • Loading branch information
Beerosagos committed Dec 17, 2024
commit 1cbdd50e094f5b804e140035ea5f490204f580ac
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
Loading