Skip to main content

bitbox_api/
lib.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Rust BitBox hardware wallet client library.
4
5#[cfg(all(feature = "wasm", feature = "multithreaded"))]
6compile_error!("wasm and multithreaded can't both be active");
7
8pub mod btc;
9pub mod cardano;
10pub mod error;
11pub mod eth;
12mod noise;
13pub mod runtime;
14#[cfg(feature = "simulator")]
15pub mod simulator;
16#[cfg(feature = "usb")]
17pub mod usb;
18#[cfg(feature = "wasm")]
19pub mod wasm;
20
21mod antiklepto;
22mod communication;
23mod constants;
24mod keypath;
25mod u2fframing;
26mod util;
27
28/// BitBox protobuf messages.
29#[allow(clippy::all)]
30pub mod pb {
31    include!("./shiftcrypto.bitbox02.rs");
32}
33
34use crate::error::{BitBoxError, Error};
35
36use pb::request::Request;
37use pb::response::Response;
38use runtime::Runtime;
39
40use noise_protocol::DH;
41use prost::Message;
42
43use futures_util::lock::{Mutex as AsyncMutex, MutexGuard as ApiMutexGuard};
44use std::sync::Mutex;
45
46pub use keypath::Keypath;
47pub use noise::PersistedNoiseConfig;
48pub use noise::{ConfigError, NoiseConfig, NoiseConfigData, NoiseConfigNoCache};
49pub use util::Threading;
50
51use communication::HwwCommunication;
52
53pub use communication::Product;
54pub use communication::{Error as CommunicationError, ReadWrite};
55
56const OP_I_CAN_HAS_HANDSHAEK: u8 = b'h';
57const OP_HER_COMEZ_TEH_HANDSHAEK: u8 = b'H';
58const OP_I_CAN_HAS_PAIRIN_VERIFICASHUN: u8 = b'v';
59const OP_NOISE_MSG: u8 = b'n';
60const _OP_ATTESTATION: u8 = b'a';
61const OP_UNLOCK: u8 = b'u';
62
63const RESPONSE_SUCCESS: u8 = 0x00;
64
65type Cipher = noise_rust_crypto::ChaCha20Poly1305;
66type HandshakeState =
67    noise_protocol::HandshakeState<noise_rust_crypto::X25519, Cipher, noise_rust_crypto::Sha256>;
68
69type CipherState = noise_protocol::CipherState<Cipher>;
70
71/// BitBox client. See `from_hid_device()`.
72pub struct BitBox<R: Runtime> {
73    communication: communication::HwwCommunication<R>,
74    noise_config: Box<dyn NoiseConfig>,
75}
76
77pub type PairingCode = String;
78
79impl<R: Runtime> BitBox<R> {
80    async fn from(
81        device: Box<dyn communication::ReadWrite>,
82        noise_config: Box<dyn NoiseConfig>,
83    ) -> Result<BitBox<R>, Error> {
84        Ok(BitBox {
85            communication: HwwCommunication::from(device).await?,
86            noise_config,
87        })
88    }
89
90    /// Creates a new BitBox instance from a custom transport.
91    ///
92    /// The `transport` is a raw byte channel to the device (HID reports); this method wraps it
93    /// in the U2F-HID framing layer internally, so callers only need to implement
94    /// [`ReadWrite`] over their platform-specific transport.
95    ///
96    /// This enables use cases where USB I/O must be delegated to platform-specific code
97    /// (e.g., Android, where USB access requires Java APIs unavailable to `hidapi`).
98    pub async fn from_transport(
99        transport: Box<dyn communication::ReadWrite>,
100        noise_config: Box<dyn NoiseConfig>,
101    ) -> Result<BitBox<R>, Error> {
102        let comm = Box::new(communication::U2fHidCommunication::from(
103            transport,
104            communication::FIRMWARE_CMD,
105        ));
106        Self::from(comm, noise_config).await
107    }
108
109    /// Creates a new BitBox instance. The provided noise config determines how the pairing
110    /// information is persisted. Use `usb::get_any_bitbox02()` to find a BitBox02 HID device.
111    ///
112    /// Use `bitbox_api::PersistedNoiseConfig::new(...)` to persist the pairing in a JSON file
113    /// (`serde` feature required) or provide your own implementation of the `NoiseConfig` trait.
114    #[cfg(feature = "usb")]
115    pub async fn from_hid_device(
116        device: hidapi::HidDevice,
117        noise_config: Box<dyn NoiseConfig>,
118    ) -> Result<BitBox<R>, Error> {
119        Self::from_transport(Box::new(crate::usb::HidDevice::new(device)), noise_config).await
120    }
121
122    #[cfg(feature = "simulator")]
123    pub async fn from_simulator(
124        endpoint: Option<&str>,
125        noise_config: Box<dyn NoiseConfig>,
126    ) -> Result<BitBox<R>, Error> {
127        Self::from_transport(
128            crate::simulator::try_connect::<R>(endpoint).await?,
129            noise_config,
130        )
131        .await
132    }
133
134    /// Invokes the device unlock and pairing.
135    pub async fn unlock_and_pair(self) -> Result<PairingBitBox<R>, Error> {
136        self.communication
137            .query(&[OP_UNLOCK])
138            .await
139            .or(Err(Error::Unknown))?;
140        self.pair().await
141    }
142
143    async fn handshake_query(&self, msg: &[u8]) -> Result<Vec<u8>, Error> {
144        let mut framed_msg = vec![OP_HER_COMEZ_TEH_HANDSHAEK];
145        framed_msg.extend_from_slice(msg);
146        let mut response = self.communication.query(&framed_msg).await?;
147        if response.is_empty() || response[0] != RESPONSE_SUCCESS {
148            return Err(Error::Noise);
149        }
150        Ok(response.split_off(1))
151    }
152
153    async fn pair(self) -> Result<PairingBitBox<R>, Error> {
154        let mut config_data = self.noise_config.read_config()?;
155        let host_static_key = match config_data.get_app_static_privkey() {
156            Some(k) => noise_rust_crypto::sensitive::Sensitive::from(k),
157            None => {
158                let k = noise_rust_crypto::X25519::genkey();
159                config_data.set_app_static_privkey(&k[..])?;
160                self.noise_config.store_config(&config_data)?;
161                k
162            }
163        };
164        let mut host = HandshakeState::new(
165            noise_protocol::patterns::noise_xx(),
166            true,
167            b"Noise_XX_25519_ChaChaPoly_SHA256",
168            Some(host_static_key),
169            None,
170            None,
171            None,
172        );
173
174        if self
175            .communication
176            .query(&[OP_I_CAN_HAS_HANDSHAEK])
177            .await?
178            .as_slice()
179            != [RESPONSE_SUCCESS]
180        {
181            return Err(Error::Noise);
182        }
183
184        let host_handshake_1 = host.write_message_vec(b"").or(Err(Error::Noise))?;
185        let bb02_handshake_1 = self.handshake_query(&host_handshake_1).await?;
186
187        host.read_message_vec(&bb02_handshake_1)
188            .or(Err(Error::Noise))?;
189        let host_handshake_2 = host.write_message_vec(b"").or(Err(Error::Noise))?;
190
191        let bb02_handshake_2 = self.handshake_query(&host_handshake_2).await?;
192        let remote_static_pubkey = host.get_rs().ok_or(Error::Noise)?;
193        let pairing_verfication_required_by_app = !self
194            .noise_config
195            .read_config()?
196            .contains_device_static_pubkey(&remote_static_pubkey);
197        let pairing_verification_required_by_device = bb02_handshake_2.as_slice() == [0x01];
198        if pairing_verfication_required_by_app || pairing_verification_required_by_device {
199            let format_hash = |h| {
200                let encoded = base32::encode(base32::Alphabet::Rfc4648 { padding: true }, h);
201                format!(
202                    "{} {}\n{} {}",
203                    &encoded[0..5],
204                    &encoded[5..10],
205                    &encoded[10..15],
206                    &encoded[15..20]
207                )
208            };
209            let handshake_hash: [u8; 32] = host.get_hash().try_into().or(Err(Error::Noise))?;
210            let pairing_code = format_hash(&handshake_hash);
211
212            Ok(PairingBitBox::from(
213                self.communication,
214                host,
215                self.noise_config,
216                Some(pairing_code),
217            ))
218        } else {
219            Ok(PairingBitBox::from(
220                self.communication,
221                host,
222                self.noise_config,
223                None,
224            ))
225        }
226    }
227}
228
229/// BitBox in the pairing state. Use `get_pairing_code()` to display the pairing code to the user
230/// and `wait_confirm()` to proceed to the paired state.
231pub struct PairingBitBox<R: Runtime> {
232    communication: communication::HwwCommunication<R>,
233    host: HandshakeState,
234    noise_config: Box<dyn NoiseConfig>,
235    pairing_code: Option<String>,
236}
237
238impl<R: Runtime> PairingBitBox<R> {
239    fn from(
240        communication: communication::HwwCommunication<R>,
241        host: HandshakeState,
242        noise_config: Box<dyn NoiseConfig>,
243        pairing_code: Option<String>,
244    ) -> Self {
245        PairingBitBox {
246            communication,
247            host,
248            noise_config,
249            pairing_code,
250        }
251    }
252
253    /// If a pairing code confirmation is required, this returns the pairing code. You must display
254    /// it to the user and then call `wait_confirm()` to wait until the user confirms the code on
255    /// the BitBox.
256    ///
257    /// If the BitBox was paired before and the pairing was persisted, the pairing step is
258    /// skipped. In this case, `None` is returned. Also in this case, call `wait_confirm()` to
259    /// establish the encrypted connection.
260    pub fn get_pairing_code(&self) -> Option<String> {
261        self.pairing_code.clone()
262    }
263
264    /// Proceed to the paired state.
265    pub async fn wait_confirm(self) -> Result<PairedBitBox<R>, Error> {
266        if self.pairing_code.is_some() {
267            let response = self
268                .communication
269                .query(&[OP_I_CAN_HAS_PAIRIN_VERIFICASHUN])
270                .await?;
271            if response.as_slice() != [RESPONSE_SUCCESS] {
272                return Err(Error::NoisePairingRejected);
273            }
274
275            let remote_static_pubkey = self.host.get_rs().ok_or(Error::Noise)?;
276            let mut config_data = self.noise_config.read_config()?;
277            config_data.add_device_static_pubkey(&remote_static_pubkey);
278            self.noise_config.store_config(&config_data)?;
279        }
280        Ok(PairedBitBox::from(self.communication, self.host))
281    }
282}
283
284/// Paired BitBox. This is where you can invoke most API functions like getting xpubs, displaying
285/// receive addresses, etc.
286pub struct PairedBitBox<R: Runtime> {
287    communication: communication::HwwCommunication<R>,
288    api_mutex: AsyncMutex<()>,
289    noise_send: Mutex<CipherState>,
290    noise_recv: Mutex<CipherState>,
291}
292
293impl<R: Runtime> PairedBitBox<R> {
294    fn from(communication: communication::HwwCommunication<R>, host: HandshakeState) -> Self {
295        let (send, recv) = host.get_ciphers();
296        PairedBitBox {
297            communication,
298            api_mutex: AsyncMutex::new(()),
299            noise_send: Mutex::new(send),
300            noise_recv: Mutex::new(recv),
301        }
302    }
303
304    /// Serializes all public API calls that touch the device.
305    ///
306    /// The BitBox protocol is an ordered request/response conversation, not a
307    /// multiplexed transport. Some public calls span multiple encrypted
308    /// requests, so this mutex guard is held for the whole public method
309    /// instead of only one query_proto() round trip.
310    pub(crate) async fn begin_api_call(&self) -> ApiMutexGuard<'_, ()> {
311        self.api_mutex.lock().await
312    }
313
314    fn validate_version(&self, comparison: &'static str) -> Result<(), Error> {
315        if semver::VersionReq::parse(comparison)
316            .or(Err(Error::Unknown))?
317            .matches(&self.communication.info.version)
318        {
319            Ok(())
320        } else {
321            Err(Error::Version(comparison))
322        }
323    }
324
325    async fn query_proto(&self, request: Request) -> Result<Response, Error> {
326        let mut encrypted = vec![OP_NOISE_MSG];
327        encrypted.extend_from_slice({
328            let mut send = self.noise_send.lock().unwrap();
329            let proto_msg = pb::Request {
330                request: Some(request),
331            };
332            &send.encrypt_vec(&proto_msg.encode_to_vec())
333        });
334
335        let response = self.communication.query(&encrypted).await?;
336        if response.is_empty() || response[0] != RESPONSE_SUCCESS {
337            return Err(Error::UnexpectedResponse);
338        }
339        let decrypted = {
340            let mut recv = self.noise_recv.lock().unwrap();
341            recv.decrypt_vec(&response[1..]).or(Err(Error::Noise))?
342        };
343        match pb::Response::decode(&decrypted[..]) {
344            Ok(pb::Response {
345                response: Some(Response::Error(pb::Error { code, .. })),
346            }) => match code {
347                101 => Err(BitBoxError::InvalidInput.into()),
348                102 => Err(BitBoxError::Memory.into()),
349                103 => Err(BitBoxError::Generic.into()),
350                104 => Err(BitBoxError::UserAbort.into()),
351                105 => Err(BitBoxError::InvalidState.into()),
352                106 => Err(BitBoxError::Disabled.into()),
353                107 => Err(BitBoxError::Duplicate.into()),
354                108 => Err(BitBoxError::NoiseEncrypt.into()),
355                109 => Err(BitBoxError::NoiseDecrypt.into()),
356                _ => Err(BitBoxError::Unknown.into()),
357            },
358            Ok(pb::Response {
359                response: Some(response),
360            }) => Ok(response),
361            _ => Err(Error::ProtobufDecode),
362        }
363    }
364
365    pub async fn device_info(&self) -> Result<pb::DeviceInfoResponse, Error> {
366        let _api_call = self.begin_api_call().await;
367        match self
368            .query_proto(Request::DeviceInfo(pb::DeviceInfoRequest {}))
369            .await?
370        {
371            Response::DeviceInfo(di) => Ok(di),
372            _ => Err(Error::UnexpectedResponse),
373        }
374    }
375
376    /// Returns which product we are connected to.
377    pub fn product(&self) -> Product {
378        self.communication.info.product
379    }
380
381    fn is_multi_edition(&self) -> bool {
382        matches!(
383            self.product(),
384            crate::Product::BitBox02Multi | crate::Product::BitBox02NovaMulti
385        )
386    }
387
388    /// Returns the firmware version.
389    pub fn version(&self) -> &semver::Version {
390        &self.communication.info.version
391    }
392
393    /// Returns the hex-encoded 4-byte root fingerprint.
394    pub async fn root_fingerprint(&self) -> Result<String, Error> {
395        let _api_call = self.begin_api_call().await;
396        self.root_fingerprint_inner().await.map(hex::encode)
397    }
398
399    pub(crate) async fn root_fingerprint_inner(&self) -> Result<Vec<u8>, Error> {
400        match self
401            .query_proto(Request::Fingerprint(pb::RootFingerprintRequest {}))
402            .await?
403        {
404            Response::Fingerprint(pb::RootFingerprintResponse { fingerprint }) => Ok(fingerprint),
405            _ => Err(Error::UnexpectedResponse),
406        }
407    }
408
409    /// Show recovery words on the Bitbox.
410    pub async fn show_mnemonic(&self) -> Result<(), Error> {
411        let _api_call = self.begin_api_call().await;
412        match self
413            .query_proto(Request::ShowMnemonic(pb::ShowMnemonicRequest {}))
414            .await?
415        {
416            Response::Success(_) => Ok(()),
417            _ => Err(Error::UnexpectedResponse),
418        }
419    }
420
421    /// Restore from recovery words on the Bitbox.
422    pub async fn restore_from_mnemonic(&self) -> Result<(), Error> {
423        let _api_call = self.begin_api_call().await;
424        let now = std::time::SystemTime::now();
425        let duration_since_epoch = now.duration_since(std::time::UNIX_EPOCH).unwrap();
426        match self
427            .query_proto(Request::RestoreFromMnemonic(
428                pb::RestoreFromMnemonicRequest {
429                    timestamp: duration_since_epoch.as_secs() as u32,
430                    timezone_offset: chrono::Local::now().offset().local_minus_utc(),
431                },
432            ))
433            .await?
434        {
435            Response::Success(_) => Ok(()),
436            _ => Err(Error::UnexpectedResponse),
437        }
438    }
439
440    /// Invokes the password change workflow on the device.
441    /// Requires firmware version >=9.25.0.
442    pub async fn change_password(&self) -> Result<(), Error> {
443        let _api_call = self.begin_api_call().await;
444        self.validate_version(">=9.25.0")?;
445        match self
446            .query_proto(Request::ChangePassword(pb::ChangePasswordRequest {}))
447            .await?
448        {
449            Response::Success(_) => Ok(()),
450            _ => Err(Error::UnexpectedResponse),
451        }
452    }
453
454    /// Invokes the BIP85-BIP39 workflow on the device, letting the user select the number of words
455    /// (12, 28, 24) and an index and display a derived BIP-39 mnemonic.
456    pub async fn bip85_app_bip39(&self) -> Result<(), Error> {
457        let _api_call = self.begin_api_call().await;
458        self.validate_version(">=9.17.0")?;
459        match self
460            .query_proto(Request::Bip85(pb::Bip85Request {
461                app: Some(pb::bip85_request::App::Bip39(())),
462            }))
463            .await?
464        {
465            Response::Bip85(pb::Bip85Response {
466                app: Some(pb::bip85_response::App::Bip39(())),
467            }) => Ok(()),
468            _ => Err(Error::UnexpectedResponse),
469        }
470    }
471}
472
473#[cfg(all(test, not(feature = "multithreaded")))]
474mod tests {
475    use super::*;
476    use crate::communication::{Error as CommunicationError, ReadWrite};
477    use crate::runtime::DefaultRuntime;
478    use crate::util::Threading;
479    use async_trait::async_trait;
480    use futures_channel::oneshot;
481    use prost::Message;
482    use std::cell::RefCell;
483    use std::rc::Rc;
484
485    struct BlockingState {
486        writes: usize,
487        reads: usize,
488        first_read_started: Option<oneshot::Sender<()>>,
489        release_first_read: Option<oneshot::Receiver<()>>,
490        response_cipher: CipherState,
491    }
492
493    struct BlockingReadWrite {
494        state: Rc<RefCell<BlockingState>>,
495    }
496
497    impl Threading for BlockingReadWrite {}
498
499    #[async_trait(?Send)]
500    impl ReadWrite for BlockingReadWrite {
501        fn write(&self, msg: &[u8]) -> Result<usize, CommunicationError> {
502            self.state.borrow_mut().writes += 1;
503            Ok(msg.len())
504        }
505
506        async fn read(&self) -> Result<Vec<u8>, CommunicationError> {
507            let (read_index, started, release) = {
508                let mut state = self.state.borrow_mut();
509                let read_index = state.reads;
510                state.reads += 1;
511                (
512                    read_index,
513                    state.first_read_started.take(),
514                    state.release_first_read.take(),
515                )
516            };
517            if read_index == 0 {
518                if let Some(started) = started {
519                    let _ = started.send(());
520                }
521                if let Some(release) = release {
522                    let _ = release.await;
523                }
524            }
525
526            let response = pb::Response {
527                response: Some(Response::DeviceInfo(pb::DeviceInfoResponse {
528                    name: "BitBox".into(),
529                    initialized: true,
530                    version: "9.26.0".into(),
531                    mnemonic_passphrase_enabled: false,
532                    monotonic_increments_remaining: 42,
533                    securechip_model: "ATECC608B".into(),
534                    bluetooth: None,
535                    password_stretching_algo: "pwhash".into(),
536                })),
537            }
538            .encode_to_vec();
539            let encrypted = self
540                .state
541                .borrow_mut()
542                .response_cipher
543                .encrypt_vec(&response);
544            let mut framed = vec![0x00, RESPONSE_SUCCESS];
545            framed.extend_from_slice(&encrypted);
546            Ok(framed)
547        }
548    }
549
550    fn paired_for_test(state: Rc<RefCell<BlockingState>>) -> PairedBitBox<DefaultRuntime> {
551        let key = [7u8; 32];
552        PairedBitBox {
553            communication: communication::HwwCommunication::from_test_parts(
554                Box::new(BlockingReadWrite { state }),
555                communication::Info {
556                    version: semver::Version::parse("9.26.0").unwrap(),
557                    product: Product::BitBox02Multi,
558                    unlocked: true,
559                    initialized: Some(true),
560                },
561            ),
562            api_mutex: AsyncMutex::new(()),
563            noise_send: Mutex::new(CipherState::new(&key, 0)),
564            noise_recv: Mutex::new(CipherState::new(&key, 0)),
565        }
566    }
567
568    #[tokio::test]
569    async fn paired_api_calls_are_serialized() {
570        let (started_tx, started_rx) = oneshot::channel();
571        let (release_tx, release_rx) = oneshot::channel();
572        let state = Rc::new(RefCell::new(BlockingState {
573            writes: 0,
574            reads: 0,
575            first_read_started: Some(started_tx),
576            release_first_read: Some(release_rx),
577            response_cipher: CipherState::new(&[7u8; 32], 0),
578        }));
579        let paired = Rc::new(paired_for_test(Rc::clone(&state)));
580
581        let local = tokio::task::LocalSet::new();
582        local
583            .run_until(async move {
584                let first = tokio::task::spawn_local({
585                    let paired = Rc::clone(&paired);
586                    async move { paired.device_info().await }
587                });
588                started_rx.await.unwrap();
589                assert_eq!(state.borrow().writes, 1);
590
591                let second = tokio::task::spawn_local({
592                    let paired = Rc::clone(&paired);
593                    async move { paired.device_info().await }
594                });
595                tokio::task::yield_now().await;
596                assert_eq!(state.borrow().writes, 1);
597
598                release_tx.send(()).unwrap();
599                first.await.unwrap().unwrap();
600                second.await.unwrap().unwrap();
601                assert_eq!(state.borrow().writes, 2);
602            })
603            .await;
604    }
605}