Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Changelog

## [Unreleased]
- Reset unfinished sessions when connecting to firmware v9.28.0 or newer, allowing host
reconnects while the device remains powered on.
- Remove the `wasm` feature, the public `wasm` module, and the TypeScript/WASM package. The
TypeScript library is now maintained in
[BitBoxSwiss/bitbox-api-ts](https://github.com/BitBoxSwiss/bitbox-api-ts/).
Expand Down
21 changes: 21 additions & 0 deletions src/communication.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ const HWW_REQ_NEW: u8 = 0x00;
const HWW_REQ_RETRY: u8 = 0x01;
// Cancel any outstanding request.
// const HWW_REQ_CANCEL: u8 = 0x02;
// Reset the previous session before starting a new one (since v9.28.0).
const HWW_REQ_RESET: u8 = 0x03;
// INFO api call (used to be OP_INFO api call), graduated to the toplevel framing so it works
// the same way for all firmware versions.
const HWW_INFO: u8 = b'i';
Expand Down Expand Up @@ -176,6 +178,23 @@ async fn get_info(communication: &dyn ReadWrite) -> Result<Info, Error> {
})
}

async fn reset_session<R: Runtime>(
communication: &dyn ReadWrite,
version: &semver::Version,
) -> Result<(), Error> {
if *version < semver::Version::new(9, 28, 0) {
return Ok(());
}
// Send at the framing layer so an unfinished workflow cannot consume the request.
loop {
match communication.query(&[HWW_REQ_RESET]).await?.as_slice() {
[HWW_RSP_ACK] => return Ok(()),
[HWW_RSP_BUSY] => R::sleep(std::time::Duration::from_secs(1)).await,
_ => return Err(Error::Info),
}
}
}

impl<R: Runtime> HwwCommunication<R> {
pub async fn from(communication: Box<dyn ReadWrite>) -> Result<Self, Error> {
let info = get_info(communication.as_ref()).await?;
Expand All @@ -187,6 +206,8 @@ impl<R: Runtime> HwwCommunication<R> {
return Err(Error::Version(">=7.0.0"));
}

reset_session::<R>(communication.as_ref(), &info.version).await?;

Ok(HwwCommunication {
communication,
info,
Expand Down
114 changes: 114 additions & 0 deletions tests/test_device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,120 @@ mod util;

use util::{test_initialized_simulators, test_simulators_after_pairing};

#[tokio::test]
async fn test_reconnect() {
use bitbox_api::{btc, pb, runtime::TokioRuntime, BitBox, NoiseConfigNoCache, PairedBitBox};
use bitbox_api::{CommunicationError, ReadWrite};
use std::sync::{Arc, Mutex};

// Record real simulator responses without replacing the transport or firmware behavior.
struct RecordingTransport {
transport: Box<bitbox_api::simulator::TcpClient>,
responses: Arc<Mutex<Vec<u8>>>,
}

impl bitbox_api::Threading for RecordingTransport {}

#[cfg_attr(feature = "multithreaded", async_trait::async_trait)]
#[cfg_attr(not(feature = "multithreaded"), async_trait::async_trait(?Send))]
impl ReadWrite for RecordingTransport {
fn write(&self, msg: &[u8]) -> Result<usize, CommunicationError> {
self.transport.write(msg)
}

async fn read(&self) -> Result<Vec<u8>, CommunicationError> {
let response = self.transport.read().await?;
self.responses.lock().unwrap().extend_from_slice(&response);
Ok(response)
}
}

async fn connect() -> PairedBitBox<TokioRuntime> {
BitBox::from_simulator(None, Box::new(NoiseConfigNoCache {}))
.await
.unwrap()
.unlock_and_pair()
.await
.unwrap()
.wait_confirm()
.await
.unwrap()
}

util::test_simulators(async |_stdout| {
let bitbox = connect().await;
bitbox.restore_from_mnemonic().await.unwrap();
if *bitbox.version() >= semver::Version::new(9, 28, 0) {
// Missing previous transaction data stops the host while the firmware is still
// waiting for the next signing request.
let transaction = btc::Transaction {
script_configs: vec![pb::BtcScriptConfigWithKeypath {
script_config: Some(btc::make_script_config_simple(
pb::btc_script_config::SimpleType::P2wpkh,
)),
keypath: bitbox_api::Keypath::try_from("m/84'/0'/0'")
.unwrap()
.to_vec(),
}],
version: 2,
inputs: vec![btc::TxInput {
prev_out_hash: vec![1; 32],
prev_out_index: 0,
prev_out_value: 100_000,
sequence: 0xffffffff,
keypath: "m/84'/0'/0'/0/0".try_into().unwrap(),
script_config_index: 0,
prev_tx: None,
}],
outputs: vec![btc::TxOutput::Internal(btc::TxInternalOutput {
keypath: "m/84'/0'/0'/1/0".try_into().unwrap(),
value: 99_000,
script_config_index: 0,
})],
locktime: 0,
};
let error = bitbox
.btc_sign(
pb::BtcCoin::Btc,
&transaction,
pb::btc_sign_init_request::FormatUnit::Default,
)
.await
.unwrap_err();
assert!(matches!(error, bitbox_api::error::Error::BtcSign(message)
if message == "input's previous transaction required but missing"));
}
drop(bitbox);

// Reconnect to the same running simulator, preserving the firmware session state.
let responses = Arc::new(Mutex::new(Vec::new()));
let bitbox = BitBox::<TokioRuntime>::from_transport(
Box::new(RecordingTransport {
transport: bitbox_api::simulator::try_connect::<TokioRuntime>(None)
.await
.unwrap(),
responses: responses.clone(),
}),
Box::new(NoiseConfigNoCache {}),
)
.await
.unwrap();
responses.lock().unwrap().clear();
let bitbox = bitbox
.unlock_and_pair()
.await
.unwrap()
.wait_confirm()
.await
.unwrap();
// The first reply must be a two-byte HWW ACK + unlock SUCCESS. An unfinished
// workflow would return its encrypted error, which unlock_and_pair currently ignores.
assert_eq!(&responses.lock().unwrap()[5..9], &[0, 2, 0, 0]);
assert_eq!(bitbox.root_fingerprint().await.unwrap(), "4c00739d");
})
.await
}

#[tokio::test]
async fn test_device_info() {
test_simulators_after_pairing(async |paired_bitbox| {
Expand Down
20 changes: 14 additions & 6 deletions tests/util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -548,10 +548,8 @@ async fn download_simulators() -> Result<Vec<String>, ()> {
Ok(filenames)
}

/// Tests on an initialized device, which is not yet seeded.
pub async fn test_simulators_after_pairing_with_stdout(
run: impl AsyncFn(&bitbox_api::PairedBitBox<bitbox_api::runtime::TokioRuntime>, &SimulatorStdout),
) {
/// Tests on a fresh simulator, allowing the test to connect and disconnect clients.
pub async fn test_simulators(run: impl AsyncFn(&SimulatorStdout)) {
let simulator_filenames = if let Some(simulator_filename) = option_env!("SIMULATOR") {
vec![simulator_filename.into()]
} else {
Expand All @@ -562,6 +560,15 @@ pub async fn test_simulators_after_pairing_with_stdout(
println!("\tSimulator tests using {simulator_filename}");
let server = Server::launch(&simulator_filename);
let stdout = server.stdout();
run(&stdout).await;
}
}

/// Tests on an initialized device, which is not yet seeded.
pub async fn test_simulators_after_pairing_with_stdout(
run: impl AsyncFn(&bitbox_api::PairedBitBox<bitbox_api::runtime::TokioRuntime>, &SimulatorStdout),
) {
test_simulators(async |stdout| {
let noise_config = Box::new(bitbox_api::NoiseConfigNoCache {});
let bitbox = bitbox_api::BitBox::<bitbox_api::runtime::TokioRuntime>::from_simulator(
None,
Expand All @@ -571,8 +578,9 @@ pub async fn test_simulators_after_pairing_with_stdout(
.unwrap();
let pairing_bitbox = bitbox.unlock_and_pair().await.unwrap();
let paired_bitbox = pairing_bitbox.wait_confirm().await.unwrap();
run(&paired_bitbox, &stdout).await;
}
run(&paired_bitbox, stdout).await;
})
.await
}

/// Tests on an initialized device, which is not yet seeded.
Expand Down
Loading