Skip to main content

bhwi_cli/
management.rs

1//! Management contexts built from local entropy, time, and PIN-position input.
2//!
3//! These helpers prepare host data; they do not perform device I/O.
4
5#[cfg(any(feature = "bitbox", feature = "keepkey", feature = "trezor"))]
6use anyhow::Context;
7use anyhow::Result;
8#[cfg(feature = "bitbox")]
9use bhwi::bitbox::{SetupEntropy, SetupMode};
10use bhwi::common::DeviceContext;
11#[cfg(any(feature = "bitbox", feature = "keepkey", feature = "trezor"))]
12use chrono::Local;
13use rand_core::{OsRng, RngCore};
14
15/// Creates Trezor setup context with 32 bytes of OS-generated entropy.
16///
17/// # Panics
18///
19/// Panics if the operating system cannot supply random entropy.
20#[cfg(feature = "trezor")]
21pub fn trezor_setup_context() -> DeviceContext {
22    let mut host_entropy = [0; 32];
23    OsRng.fill_bytes(&mut host_entropy);
24    bhwi_async::management::trezor_setup_context(host_entropy)
25}
26
27/// Creates Trezor PIN context from nonempty ASCII-digit keypad positions.
28///
29/// Positions are not literal PIN digits; no 1–9 range check is performed.
30///
31/// # Errors
32///
33/// Returns an error for empty input or non-ASCII digits.
34#[cfg(feature = "trezor")]
35pub fn trezor_pin_context(positions: String) -> Result<DeviceContext> {
36    Ok(bhwi_async::management::trezor_pin_context_from_positions(
37        positions,
38    )?)
39}
40
41/// Creates Trezor recovery context using the current Unix time as its U2F counter.
42///
43/// # Errors
44///
45/// Returns an error if the timestamp does not fit in `u32`.
46#[cfg(feature = "trezor")]
47pub fn trezor_restore_context() -> Result<DeviceContext> {
48    let u2f_counter = u2f_counter_from(Local::now().timestamp())?;
49    Ok(bhwi_async::management::trezor_restore_context(u2f_counter))
50}
51
52/// Creates KeepKey setup context with 32 bytes of OS-generated entropy.
53///
54/// # Panics
55///
56/// Panics if the operating system cannot supply random entropy.
57#[cfg(feature = "keepkey")]
58pub fn keepkey_setup_context() -> DeviceContext {
59    let mut host_entropy = [0; 32];
60    OsRng.fill_bytes(&mut host_entropy);
61    bhwi_async::management::keepkey_setup_context(host_entropy)
62}
63
64/// Creates KeepKey PIN context from nonempty ASCII-digit keypad positions.
65///
66/// Positions are not literal PIN digits; no 1–9 range check is performed.
67///
68/// # Errors
69///
70/// Returns an error for empty input or non-ASCII digits.
71#[cfg(feature = "keepkey")]
72pub fn keepkey_pin_context(positions: String) -> Result<DeviceContext> {
73    Ok(bhwi_async::management::keepkey_pin_context_from_positions(
74        positions,
75    )?)
76}
77
78/// Creates KeepKey recovery context using the current Unix time as its U2F counter.
79///
80/// # Errors
81///
82/// Returns an error if the timestamp does not fit in `u32`.
83#[cfg(feature = "keepkey")]
84pub fn keepkey_restore_context() -> Result<DeviceContext> {
85    let u2f_counter = u2f_counter_from(Local::now().timestamp())?;
86    Ok(bhwi_async::management::keepkey_restore_context(u2f_counter))
87}
88
89#[cfg(any(feature = "keepkey", feature = "trezor"))]
90fn u2f_counter_from(timestamp: i64) -> Result<u32> {
91    u32::try_from(timestamp).context("current timestamp does not fit in u32")
92}
93
94/// Creates BitBox02 setup context with the current Unix time and local UTC offset.
95///
96/// Both time values are in seconds. Emulators use mnemonic restoration; physical
97/// devices use new-wallet setup with 32 bytes of OS-generated entropy.
98///
99/// # Errors
100///
101/// Returns an error if the timestamp does not fit in `u32`.
102///
103/// # Panics
104///
105/// Panics if a physical device is selected and the operating system cannot supply entropy.
106#[cfg(feature = "bitbox")]
107pub fn bitbox_setup_context(is_emulated: bool) -> Result<DeviceContext> {
108    let (timestamp, timezone_offset) = timestamp_and_timezone_offset()?;
109    let mode = if is_emulated {
110        SetupMode::RestoreFromMnemonic
111    } else {
112        let mut entropy = [0; 32];
113        OsRng.fill_bytes(&mut entropy);
114        SetupMode::NewWallet {
115            entropy: SetupEntropy::new(entropy),
116        }
117    };
118    Ok(bhwi_async::management::bitbox_setup_context(
119        mode,
120        timestamp,
121        timezone_offset,
122    ))
123}
124
125/// Creates BitBox02 mnemonic-restoration context with the current time and UTC offset.
126///
127/// Both time values are in seconds.
128///
129/// # Errors
130///
131/// Returns an error if the Unix timestamp does not fit in `u32`.
132#[cfg(feature = "bitbox")]
133pub fn bitbox_restore_context() -> Result<DeviceContext> {
134    let (timestamp, timezone_offset) = timestamp_and_timezone_offset()?;
135    Ok(bhwi_async::management::bitbox_restore_context(
136        timestamp,
137        timezone_offset,
138    ))
139}
140
141#[cfg(feature = "bitbox")]
142fn timestamp_and_timezone_offset() -> Result<(u32, i32)> {
143    let now = Local::now();
144    timestamp_and_timezone_offset_from(now.timestamp(), now.offset().local_minus_utc())
145}
146
147#[cfg(feature = "bitbox")]
148fn timestamp_and_timezone_offset_from(timestamp: i64, timezone_offset: i32) -> Result<(u32, i32)> {
149    let timestamp = u32::try_from(timestamp).context("current timestamp does not fit in u32")?;
150    Ok((timestamp, timezone_offset))
151}
152
153#[cfg(all(test, feature = "bitbox"))]
154mod tests {
155    use super::*;
156    use bhwi::bitbox::ManagementContext;
157
158    #[test]
159    fn simulator_setup_uses_mnemonic_restore_without_entropy() {
160        assert!(matches!(
161            bitbox_setup_context(true).unwrap(),
162            DeviceContext::BitBoxManagement(ManagementContext::Setup {
163                mode: SetupMode::RestoreFromMnemonic,
164                ..
165            })
166        ));
167        assert!(matches!(
168            bitbox_setup_context(false).unwrap(),
169            DeviceContext::BitBoxManagement(ManagementContext::Setup {
170                mode: SetupMode::NewWallet { .. },
171                ..
172            })
173        ));
174    }
175
176    #[test]
177    fn timestamp_and_timezone_offset_preserves_seconds_east_of_utc() {
178        assert_eq!(
179            timestamp_and_timezone_offset_from(1_750_000_000, 3 * 60 * 60).unwrap(),
180            (1_750_000_000, 3 * 60 * 60)
181        );
182        assert_eq!(
183            timestamp_and_timezone_offset_from(1_750_000_000, -7 * 60 * 60).unwrap(),
184            (1_750_000_000, -7 * 60 * 60)
185        );
186    }
187
188    #[test]
189    fn restore_context_contains_host_time() {
190        assert!(matches!(
191            bitbox_restore_context().unwrap(),
192            DeviceContext::BitBoxManagement(ManagementContext::Restore { timestamp, .. })
193                if timestamp > 0
194        ));
195    }
196
197    #[test]
198    #[cfg(feature = "keepkey")]
199    fn keepkey_contexts_use_keepkey_management_variants() {
200        assert!(matches!(
201            keepkey_setup_context(),
202            DeviceContext::KeepKeyManagement(bhwi::keepkey::ManagementContext::Setup { .. })
203        ));
204        assert!(matches!(
205            keepkey_restore_context().unwrap(),
206            DeviceContext::KeepKeyManagement(bhwi::keepkey::ManagementContext::Restore {
207                u2f_counter
208            }) if u2f_counter > 0
209        ));
210        assert!(matches!(
211            keepkey_pin_context("7913".to_owned()).unwrap(),
212            DeviceContext::KeepKeyManagement(bhwi::keepkey::ManagementContext::Pin(_))
213        ));
214        assert!(keepkey_pin_context("not-a-pin".to_owned()).is_err());
215    }
216}