diff --git a/.gitignore b/.gitignore index fd916a87ba..c17fdbcfaa 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ .ccls-cache compile_commands.json compile_flags.txt +.vscode +.DS_Store # gnu global /src/GPATH diff --git a/py/send_message.py b/py/send_message.py index a56f66487c..47fa429ce9 100755 --- a/py/send_message.py +++ b/py/send_message.py @@ -18,6 +18,7 @@ # pylint: disable=too-many-lines import argparse +import socket import pprint import sys from typing import List, Any, Optional, Callable, Union, Tuple, Sequence @@ -41,6 +42,7 @@ FirmwareVersionOutdatedException, u2fhid, bitbox_api_protocol, + PhysicalLayer, ) import u2f @@ -1556,6 +1558,65 @@ def run(self) -> int: return 0 +def connect_to_simulator_bitbox(debug: bool) -> int: + """ + Connects and runs the main menu on host computer, + simulating a BitBox02 connected over USB. + """ + + class Simulator(PhysicalLayer): + """ + Simulator class handles the communication + with the firmware simulator + """ + + def __init__(self) -> None: + self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + port = 15423 + self.client_socket.bind(("", port)) + self.client_socket.listen(50) + print(f"Waiting for connection on port {port}") + self.connection, addr = self.client_socket.accept() + print(f"Connected to {addr}") + + def write(self, data: bytes) -> None: + self.connection.send(data[1:]) + if debug: + print(f"Written to the simulator:\n{data.hex()[2:]}") + + def read(self, size: int, timeout_ms: int) -> bytes: + res = self.connection.recv(64) + if debug: + print(f"Read from the simulator:\n{res.hex()}") + return res + + def __del__(self) -> None: + print("Simulator quit") + if self.connection: + self.connection.shutdown(socket.SHUT_RDWR) + self.connection.close() + + simulator = Simulator() + + device_info: devices.DeviceInfo = { + "serial_number": "v9.16.0", + "path": b"", + "product_string": "BitBox02BTC", + } + noise_config = bitbox_api_protocol.BitBoxNoiseConfig() + bitbox_connection = bitbox02.BitBox02( + transport=u2fhid.U2FHid(simulator), + device_info=device_info, + noise_config=noise_config, + ) + try: + bitbox_connection.check_min_version() + except FirmwareVersionOutdatedException as exc: + print("WARNING: ", exc) + + return SendMessage(bitbox_connection, debug).run() + + def connect_to_usb_bitbox(debug: bool, use_cache: bool) -> int: """ Connects and runs the main menu on a BitBox02 connected @@ -1643,6 +1704,11 @@ def main() -> int: parser = argparse.ArgumentParser(description="Tool for communicating with bitbox device") parser.add_argument("--debug", action="store_true", help="Print messages sent and received") parser.add_argument("--u2f", action="store_true", help="Use u2f menu instead") + parser.add_argument( + "--simulator", + action="store_true", + help="Connect to the BitBox02 simulator instead of a real BitBox02", + ) parser.add_argument( "--no-cache", action="store_true", help="Don't use cached or store noise keys" ) @@ -1663,6 +1729,9 @@ def main() -> int: return u2fapp.run() return 1 + if args.simulator: + return connect_to_simulator_bitbox(args.debug) + return connect_to_usb_bitbox(args.debug, not args.no_cache) diff --git a/src/rust/bitbox02-rust/src/workflow.rs b/src/rust/bitbox02-rust/src/workflow.rs index 352ca4ad58..edfbfa103f 100644 --- a/src/rust/bitbox02-rust/src/workflow.rs +++ b/src/rust/bitbox02-rust/src/workflow.rs @@ -15,6 +15,7 @@ pub mod cancel; pub mod confirm; pub mod menu; +#[cfg_attr(feature = "c-unit-testing", path = "workflow/mnemonic_c_unit_tests.rs")] pub mod mnemonic; pub mod pairing; pub mod password; diff --git a/src/rust/bitbox02-rust/src/workflow/confirm.rs b/src/rust/bitbox02-rust/src/workflow/confirm.rs index 19dc14701e..ef3ae196c5 100644 --- a/src/rust/bitbox02-rust/src/workflow/confirm.rs +++ b/src/rust/bitbox02-rust/src/workflow/confirm.rs @@ -31,5 +31,7 @@ pub async fn confirm(params: &Params<'_>) -> Result<(), UserAbort> { }; }); component.screen_stack_push(); + #[cfg(feature = "c-unit-testing")] + bitbox02::print_stdout(&format!("CONFIRM SCREEN START\nTITLE: {}\nBODY: {}\nCONFIRM SCREEN END\n", params.title, params.body)); option_no_screensaver(&result).await } diff --git a/src/rust/bitbox02-rust/src/workflow/mnemonic.rs b/src/rust/bitbox02-rust/src/workflow/mnemonic.rs index 075b28384a..0b003f1c8d 100644 --- a/src/rust/bitbox02-rust/src/workflow/mnemonic.rs +++ b/src/rust/bitbox02-rust/src/workflow/mnemonic.rs @@ -502,4 +502,4 @@ mod tests { &bruteforce_lastword(&mnemonic) ); } -} +} \ No newline at end of file diff --git a/src/rust/bitbox02-rust/src/workflow/mnemonic_c_unit_tests.rs b/src/rust/bitbox02-rust/src/workflow/mnemonic_c_unit_tests.rs new file mode 100644 index 0000000000..10af0b1b8f --- /dev/null +++ b/src/rust/bitbox02-rust/src/workflow/mnemonic_c_unit_tests.rs @@ -0,0 +1,48 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +pub use super::cancel::Error as CancelError; +use super::confirm; + +use alloc::string::String; +use alloc::string::ToString; + +pub async fn show_and_confirm_mnemonic(words: &[&str]) -> Result<(), CancelError> { + let _ = confirm::confirm(&confirm::Params { + title: "", + body: "Please confirm\neach word", + accept_only: true, + accept_is_nextarrow: true, + ..Default::default() + }) + .await; + + for word in words.iter() { + bitbox02::println_stdout(word); + } + bitbox02::println_stdout("Words confirmed"); + + Ok(()) +} + +pub async fn get() -> Result, CancelError> { + let words = "boring mistake dish oyster truth pigeon viable emerge sort crash wire portion cannon couple enact box walk height pull today solid off enable tide"; + bitbox02::println_stdout("Restored from recovery words below:"); + bitbox02::println_stdout(words); + + Ok(zeroize::Zeroizing::new( + words + .to_string() + )) +} \ No newline at end of file diff --git a/src/rust/bitbox02-rust/src/workflow/status.rs b/src/rust/bitbox02-rust/src/workflow/status.rs index d5a38ec30f..4bac5b8e91 100644 --- a/src/rust/bitbox02-rust/src/workflow/status.rs +++ b/src/rust/bitbox02-rust/src/workflow/status.rs @@ -21,5 +21,7 @@ pub async fn status(title: &str, status_success: bool) { *result.borrow_mut() = Some(()); }); component.screen_stack_push(); + #[cfg(feature = "c-unit-testing")] + bitbox02::print_stdout(&format!("STATUS SCREEN START\nTITLE: {}\nSTATUS SCREEN END\n", title)); option_no_screensaver(&result).await } diff --git a/src/rust/bitbox02-rust/src/workflow/trinary_input_string.rs b/src/rust/bitbox02-rust/src/workflow/trinary_input_string.rs index 62f8d6f259..9b300d5f89 100644 --- a/src/rust/bitbox02-rust/src/workflow/trinary_input_string.rs +++ b/src/rust/bitbox02-rust/src/workflow/trinary_input_string.rs @@ -49,6 +49,8 @@ pub async fn enter( bitbox02::ui::trinary_input_string_set_input(&mut component, preset); } component.screen_stack_push(); + #[cfg(feature = "c-unit-testing")] + bitbox02::print_stdout(&format!("ENTER SCREEN START\nTITLE: {}\nENTER SCREEN END\n", params.title)); option(&result) .await .or(Err(super::cancel::Error::Cancelled)) diff --git a/src/rust/bitbox02/src/lib.rs b/src/rust/bitbox02/src/lib.rs index e5e05280d1..5c46f21429 100644 --- a/src/rust/bitbox02/src/lib.rs +++ b/src/rust/bitbox02/src/lib.rs @@ -183,6 +183,14 @@ pub fn print_stdout(msg: &str) { } } +#[cfg(any(feature = "testing", feature = "c-unit-testing"))] +pub fn println_stdout(msg: &str) { + unsafe { + bitbox02_sys::printf(crate::util::str_to_cstr_vec(msg).unwrap().as_ptr()); + bitbox02_sys::printf(crate::util::str_to_cstr_vec("\n").unwrap().as_ptr()); + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/rust/bitbox02/src/ui.rs b/src/rust/bitbox02/src/ui.rs index ba3af064d0..5d9241d7c8 100644 --- a/src/rust/bitbox02/src/ui.rs +++ b/src/rust/bitbox02/src/ui.rs @@ -16,6 +16,7 @@ mod types; #[cfg_attr(feature = "testing", path = "ui/ui_stub.rs")] +#[cfg_attr(not(feature = "testing"), cfg_attr(feature = "c-unit-testing", path = "ui/ui_stub_c_unit_tests.rs"))] // We don't actually use ui::ui anywhere, we re-export below. #[allow(clippy::module_inception)] mod ui; diff --git a/src/rust/bitbox02/src/ui/types.rs b/src/rust/bitbox02/src/ui/types.rs index c51c56231d..d4972f21eb 100644 --- a/src/rust/bitbox02/src/ui/types.rs +++ b/src/rust/bitbox02/src/ui/types.rs @@ -21,7 +21,7 @@ use util::Survive; pub use bitbox02_sys::trinary_choice_t as TrinaryChoice; // Taking the constant straight from C, as it's excluding the null terminator. -#[cfg_attr(feature = "testing", allow(dead_code))] +#[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))] pub(crate) const MAX_LABEL_SIZE: usize = bitbox02_sys::MAX_LABEL_SIZE as _; #[derive(Default)] @@ -33,7 +33,7 @@ pub enum Font { } impl Font { - #[cfg_attr(feature = "testing", allow(dead_code))] + #[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))] pub(crate) fn as_ptr(&self) -> *const bitbox02_sys::UG_FONT { match self { Font::Default => core::ptr::null() as *const _, @@ -65,7 +65,7 @@ pub struct ConfirmParams<'a> { } impl<'a> ConfirmParams<'a> { - #[cfg_attr(feature = "testing", allow(dead_code))] + #[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))] /// `title_scratch` and `body_scratch` exist to keep the data /// alive for as long as the C params live. pub(crate) fn to_c_params( @@ -110,7 +110,7 @@ pub struct TrinaryInputStringParams<'a> { } impl<'a> TrinaryInputStringParams<'a> { - #[cfg_attr(feature = "testing", allow(dead_code))] + #[cfg_attr(any(feature = "testing", feature = "c-unit-testing"), allow(dead_code))] pub(crate) fn to_c_params( &self, title_scratch: &'a mut Vec, diff --git a/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs new file mode 100644 index 0000000000..19100db51f --- /dev/null +++ b/src/rust/bitbox02/src/ui/ui_stub_c_unit_tests.rs @@ -0,0 +1,163 @@ +// 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Stubs for the Bitbox02 simulator and also C unit-tests. + +pub use super::types::{ + AcceptRejectCb, ConfirmParams, ContinueCancelCb, Font, MenuParams, SelectWordCb, TrinaryChoice, + TrinaryChoiceCb, TrinaryInputStringParams, +}; + +use crate::input::SafeInputString; + +use core::marker::PhantomData; + +extern crate alloc; + +pub struct Component<'a> { + is_pushed: bool, + _p: PhantomData<&'a ()>, +} + +impl<'a> Component<'a> { + pub fn screen_stack_push(&mut self) { + if self.is_pushed { + panic!("component pushed twice"); + } + self.is_pushed = true; + } +} + +impl<'a> Drop for Component<'a> { + fn drop(&mut self) { + if !self.is_pushed { + panic!("component not pushed"); + } + } +} + +pub fn trinary_input_string_create<'a, F>( + _params: &TrinaryInputStringParams, + mut confirm_callback: F, + _cancel_callback: Option>, +) -> Component<'a> +where + F: FnMut(SafeInputString) + 'a, +{ + confirm_callback(SafeInputString::new()); + Component { + is_pushed: false, + _p: PhantomData, + } +} + +pub fn confirm_create<'a, F>(_params: &ConfirmParams, mut result_callback: F) -> Component<'a> +where + F: FnMut(bool) + 'a, +{ + result_callback(true); + Component { + is_pushed: false, + _p: PhantomData, + } +} + +pub fn screen_process() {} + +pub fn status_create<'a, F>(_text: &str, _status_success: bool, mut callback: F) -> Component<'a> +where + F: FnMut() + 'a, +{ + callback(); + Component { + is_pushed: false, + _p: PhantomData, + } +} + +pub fn sdcard_create<'a, F>(_insert: bool, mut continue_callback: F) -> Component<'a> +where + F: FnMut() + 'a, +{ + continue_callback(); + Component { + is_pushed: false, + _p: PhantomData, + } +} + +pub fn menu_create(_params: MenuParams<'_>) -> Component<'_> { + panic!("not implemented"); +} + +pub fn trinary_choice_create<'a>( + _message: &'a str, + _label_left: &'a str, + _label_middle: &'a str, + _label_right: &'a str, + _chosen_callback: TrinaryChoiceCb, +) -> Component<'a> { + panic!("not implemented") +} + +pub fn confirm_transaction_address_create<'a, 'b>( + _amount: &'a str, + _address: &'a str, + mut callback: AcceptRejectCb<'b>, +) -> Component<'b> { + callback(true); + Component { + is_pushed: false, + _p: PhantomData, + } +} + +pub fn confirm_transaction_fee_create<'a, 'b>( + _amount: &'a str, + _fee: &'a str, + _longtouch: bool, + mut callback: AcceptRejectCb<'b>, +) -> Component<'b> { + callback(true); + Component { + is_pushed: false, + _p: PhantomData, + } +} + +pub fn trinary_input_string_set_input(_component: &mut Component, _word: &str) { + panic!("not implemented") +} + +pub fn with_lock_animation(f: F) { + f() +} + +pub fn screen_stack_pop_all() {} + +pub fn progress_create<'a>(_title: &str) -> Component<'a> { + Component { + is_pushed: false, + _p: PhantomData, + } +} + +pub fn progress_set(_component: &mut Component, _progress: f32) {} + +pub fn empty_create<'a>() -> Component<'a> { + Component { + is_pushed: false, + _p: PhantomData, + } +} \ No newline at end of file diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt index 8f79f0516e..1144125115 100644 --- a/test/unit-test/CMakeLists.txt +++ b/test/unit-test/CMakeLists.txt @@ -240,6 +240,8 @@ set(TEST_LIST "-Wl,--wrap=screen_process" ugui "" + simulator + "" ) find_package(CMocka REQUIRED) @@ -263,7 +265,9 @@ foreach(I RANGE 0 ${TEST_LIST_LEN} 2) ${CMOCKA_LIBRARIES} ${TEST_LINK_ARGS} ) - add_test(NAME test_${TEST_NAME} COMMAND ${EXE}) + if(NOT ${TEST_NAME} STREQUAL "simulator") + add_test(NAME test_${TEST_NAME} COMMAND ${EXE}) + endif() endforeach() diff --git a/test/unit-test/framework/mock_securechip.c b/test/unit-test/framework/mock_securechip.c index 957c42d840..8709b261a0 100644 --- a/test/unit-test/framework/mock_securechip.c +++ b/test/unit-test/framework/mock_securechip.c @@ -86,10 +86,12 @@ bool securechip_attestation_sign(const uint8_t* msg, uint8_t* signature_out) bool securechip_monotonic_increments_remaining(uint32_t* remaining_out) { - return false; + *remaining_out = 1; + return true; } bool securechip_model(securechip_model_t* model_out) { - return false; + *model_out = SECURECHIP_ATECC608B; + return true; } diff --git a/test/unit-test/test_simulator.c b/test/unit-test/test_simulator.c new file mode 100644 index 0000000000..346f88f06c --- /dev/null +++ b/test/unit-test/test_simulator.c @@ -0,0 +1,155 @@ +// Copyright 2023-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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "hww.h" +#include "memory/bitbox02_smarteeprom.h" +#include "usb/usb_packet.c" +#include "usb/usb_processing.c" +#include "usb/usb_processing.h" +#include "workflow/idle_workflow.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#define BUFFER_SIZE 1024 +#define SOCKET + +static uint8_t _out_report[USB_HID_REPORT_OUT_SIZE]; +char out_buffer[2 * USB_HID_REPORT_OUT_SIZE]; +int data_len; +int sockfd; + +int get_usb_message_socket(char* buffer, uint8_t* input) +{ + int num_read = read(sockfd, buffer, USB_HID_REPORT_OUT_SIZE); + for (int i = 0; i < USB_HID_REPORT_OUT_SIZE; i++) { + input[i] = (uint8_t)buffer[i]; + } + return num_read; +} + +void send_usb_message_socket(void) +{ + struct queue* q = queue_hww_queue(); + const uint8_t* data = queue_pull(q); + if (data != NULL) { + data_len = 256 * (int)data[5] + (int)data[6]; + for (int i = 0; i < USB_HID_REPORT_OUT_SIZE; i++) { + out_buffer[i] = (char)data[i]; + } + if (!write(sockfd, out_buffer, USB_HID_REPORT_OUT_SIZE)) { + perror("ERROR, could not write to socket"); + exit(1); + } + } +} + +void simulate_firmware_execution(uint8_t* input) +{ + memcpy(_out_report, input, sizeof(_out_report)); + usb_packet_process((const USB_FRAME*)_out_report); + rust_workflow_spin(); + rust_async_usb_spin(); + usb_processing_process(usb_processing_hww()); +} + +int main(void) +{ + char buffer[BUFFER_SIZE]; + uint8_t input[BUFFER_SIZE]; + int (*get_message)(char*, uint8_t*); + void (*send_message)(void); + bool memory_success, sd_success; + int temp_len; + + // Establish socket connection with client + int portno; + struct sockaddr_in serv_addr; + struct hostent* server; + portno = 15423; + sockfd = socket(AF_INET, SOCK_STREAM, 0); + if (sockfd < 0) { + perror("ERROR opening socket"); + return 1; + } + server = gethostbyname("host.docker.internal"); + if (server == NULL) { + fprintf(stderr, "ERROR, no such host\n"); + return 1; + } + memset((char*)&serv_addr, 0, sizeof(serv_addr)); + serv_addr.sin_family = AF_INET; + memcpy((char*)&serv_addr.sin_addr.s_addr, (char*)server->h_addr_list[0], server->h_length); + serv_addr.sin_port = htons(portno); + if (connect(sockfd, (struct sockaddr*)&serv_addr, sizeof(serv_addr)) < 0) { + perror("ERROR, could not connect to client"); + return 1; + } + + get_message = get_usb_message_socket; + send_message = send_usb_message_socket; + + // BitBox02 simulation initializaition + usb_processing_init(); + usb_processing_set_send(usb_processing_hww(), send_message); + printf("USB setup success\n"); + + hww_setup(); + printf("HWW setup success\n"); + + sd_success = sd_format(); + printf("Sd card setup %s\n", sd_success ? "success" : "failed"); + if (!sd_success) { + perror("ERROR, sd card setup failed"); + return 1; + } + + mock_memory_factoryreset(); + memory_interface_functions_t ifs = { + .random_32_bytes = random_32_bytes_mcu, + }; + memory_success = memory_setup(&ifs); + printf("Memory setup %s\n", memory_success ? "success" : "failed"); + if (!memory_success) { + perror("ERROR, memory setup failed"); + return 1; + } + + smarteeprom_bb02_config(); + bitbox02_smarteeprom_init(); + idle_workflow_blocking(); + + while (1) { + if (!get_message(buffer, input)) break; + simulate_firmware_execution(input); + + temp_len = data_len - (USB_HID_REPORT_OUT_SIZE - 7); + while (temp_len > 0) { + usb_processing_process(usb_processing_hww()); + temp_len -= (USB_HID_REPORT_OUT_SIZE - 5); + } + } + close(sockfd); + return 0; +} \ No newline at end of file