1use crate::apdu::{Apdu, Response, StatusWord};
4use crate::error::{Error, Result};
5use crate::os::{
6 self, AppAndVersion, BatteryStatus, BatteryStatusKind, EndorsementInfo, EndorsementSlot,
7 InstalledApp, LanguagePack, OnboardIdentity, OsVersion, ScreenSaver, ScreenSaverAnimation,
8 ScreenSaverSpeed,
9};
10use crate::transport::Transport;
11
12pub struct Device<T: Transport> {
14 transport: T,
15}
16
17impl<T: Transport> Device<T> {
18 pub fn new(transport: T) -> Self {
19 Self { transport }
20 }
21
22 pub fn transport(&self) -> &T {
23 &self.transport
24 }
25
26 pub fn into_inner(self) -> T {
27 self.transport
28 }
29
30 pub async fn exchange_raw(&self, apdu: &[u8]) -> Result<Vec<u8>> {
32 self.transport.exchange(apdu).await
33 }
34
35 pub async fn exchange(&self, apdu: &Apdu) -> Result<Response> {
37 let raw = self.transport.exchange(&apdu.encode()?).await?;
38 Response::from_raw(raw)
39 }
40
41 pub async fn send(&self, apdu: &Apdu) -> Result<Vec<u8>> {
43 self.exchange(apdu).await?.ok()
44 }
45
46 pub async fn get_version(&self) -> Result<OsVersion> {
48 let r = self.exchange(&os::GET_VERSION).await?;
49 match r.sw {
50 StatusWord::DEVICE_NOT_ONBOARDED | StatusWord::DEVICE_NOT_ONBOARDED_2 => {
51 Err(Error::App("device is not onboarded".into()))
52 }
53 StatusWord::CLA_NOT_SUPPORTED | StatusWord::INS_NOT_SUPPORTED => {
54 Err(Error::DashboardExpected)
55 }
56 _ => OsVersion::parse(&r.ok()?),
57 }
58 }
59
60 pub async fn get_app_and_version(&self) -> Result<AppAndVersion> {
62 AppAndVersion::parse(&self.send(&os::GET_APP_AND_VERSION).await?)
63 }
64
65 pub async fn is_on_dashboard(&self) -> Result<bool> {
67 let r = self.exchange(&os::GET_APP_AND_VERSION).await?;
68 match r.sw {
69 StatusWord::OK => Ok(AppAndVersion::parse(&r.data)?.is_dashboard()),
70 StatusWord::CLA_NOT_SUPPORTED | StatusWord::CLA_NOT_SUPPORTED_BOOTLOADER => Ok(true),
71 StatusWord::INS_NOT_SUPPORTED => Ok(false),
72 sw => Err(sw.into()),
73 }
74 }
75
76 pub async fn quit_app(&self) -> Result<()> {
81 self.send(&os::QUIT_APP).await.map(|_| ())
82 }
83
84 pub async fn open_app(&self, name: &str) -> Result<()> {
87 let r = self.exchange(&os::open_app(name)).await?;
88 match r.sw {
89 StatusWord::OK => Ok(()),
90 StatusWord::APP_NOT_FOUND | StatusWord(0x6984) => {
91 Err(Error::NotFound(format!("app {name:?} is not installed")))
92 }
93 sw if sw.is_refused() => Err(Error::UserRefused),
94 sw => Err(sw.into()),
95 }
96 }
97
98 pub async fn list_apps(&self) -> Result<Vec<InstalledApp>> {
101 let mut apps = Vec::new();
102 let mut r = self.exchange(&os::LIST_APPS).await?;
103 loop {
104 match r.sw {
105 StatusWord::OK => {}
106 sw if sw.is_refused() => return Err(Error::UserRefused),
107 sw => return Err(sw.into()),
108 }
109 if r.data.is_empty() {
110 break;
111 }
112 apps.extend(os::parse_list_apps(&r.data)?);
113 r = self.exchange(&os::LIST_APPS_CONTINUE).await?;
114 }
115 Ok(apps)
116 }
117
118 pub async fn get_device_name(&self) -> Result<String> {
121 let data = self.send(&os::GET_DEVICE_NAME).await?;
122 Ok(String::from_utf8_lossy(&data).into_owned())
123 }
124
125 pub async fn set_device_name(&self, name: &str) -> Result<()> {
127 if name.is_empty() || name.len() > 32 {
128 return Err(Error::InvalidInput("name must be 1..=32 bytes".into()));
129 }
130 match self.exchange(&os::set_device_name(name)).await?.sw {
131 StatusWord::OK => Ok(()),
132 sw if sw.is_refused() => Err(Error::UserRefused),
133 sw => Err(sw.into()),
134 }
135 }
136
137 fn screen_saver_err(sw: StatusWord) -> Error {
138 match sw {
139 sw if sw.is_refused() => Error::UserRefused,
140 StatusWord(0x6d00..=0x6dff) => Error::App(
143 "screensaver command rejected: only accepted in recovery mode (Nano X / Nano S Plus), with a valid request"
144 .into(),
145 ),
146 sw => sw.into(),
147 }
148 }
149
150 pub async fn get_screen_saver(&self) -> Result<Option<ScreenSaver>> {
152 let r = self.exchange(&os::get_screen_saver()).await?;
153 if !r.sw.is_ok() {
154 return Err(Self::screen_saver_err(r.sw));
155 }
156 ScreenSaver::parse(&r.data)
157 }
158
159 pub async fn set_screen_saver(
162 &self,
163 text: &str,
164 animation: ScreenSaverAnimation,
165 speed: ScreenSaverSpeed,
166 ) -> Result<()> {
167 let r = self
168 .exchange(&os::set_screen_saver(text, animation, speed)?)
169 .await?;
170 if r.sw.is_ok() {
171 Ok(())
172 } else {
173 Err(Self::screen_saver_err(r.sw))
174 }
175 }
176
177 pub async fn reset_screen_saver(&self) -> Result<()> {
179 let r = self.exchange(&os::reset_screen_saver()).await?;
180 if r.sw.is_ok() {
181 Ok(())
182 } else {
183 Err(Self::screen_saver_err(r.sw))
184 }
185 }
186
187 fn check(&self, r: crate::apdu::Response) -> Result<Vec<u8>> {
188 match r.sw {
189 StatusWord::OK => Ok(r.data),
190 sw if sw.is_refused() => Err(Error::UserRefused),
191 sw => Err(sw.into()),
192 }
193 }
194
195 pub async fn reset(&self) -> Result<()> {
197 match self.exchange(&os::RESET).await {
198 Ok(r) => self.check(r).map(|_| ()),
199 Err(Error::Disconnected) => Ok(()),
200 Err(e) => Err(e),
201 }
202 }
203
204 pub async fn get_device_mac(&self) -> Result<String> {
206 let d = self.check(self.exchange(&os::GET_DEVICE_MAC).await?)?;
207 Ok(d.iter()
208 .map(|b| format!("{b:02x}"))
209 .collect::<Vec<_>>()
210 .join(":"))
211 }
212
213 pub async fn set_battery_charge_limit(&self, percent: u8) -> Result<()> {
215 self.check(
216 self.exchange(&os::set_battery_charge_limit(percent)?)
217 .await?,
218 )
219 .map(|_| ())
220 }
221
222 pub async fn list_language_packs(&self) -> Result<Vec<LanguagePack>> {
224 let mut out = Vec::new();
225 let mut first = true;
226 loop {
227 let d = self.check(self.exchange(&os::list_language_packs(first)).await?)?;
228 match os::parse_language_pack(&d)? {
229 Some(p) => out.push(p),
230 None => break,
231 }
232 first = false;
233 if out.len() > 32 {
234 break;
235 }
236 }
237 Ok(out)
238 }
239
240 pub async fn run_apdu_script(
244 &self,
245 script: &str,
246 on_progress: &mut dyn FnMut(usize, usize),
247 ) -> Result<()> {
248 let apdus: Vec<Vec<u8>> = script
249 .lines()
250 .map(|l| l.split('#').next().unwrap_or_default().trim())
251 .filter(|l| !l.is_empty())
252 .map(|l| hex::decode(l.replace(' ', "")))
253 .collect::<core::result::Result<_, _>>()
254 .map_err(|e| Error::InvalidInput(format!("APDU script: {e}")))?;
255 if apdus.is_empty() {
256 return Err(Error::InvalidInput("empty APDU script".into()));
257 }
258 let total = apdus.len();
259 for (i, apdu) in apdus.iter().enumerate() {
260 let r = crate::apdu::Response::from_raw(self.exchange_raw(apdu).await?)?;
261 match r.sw.0 {
262 0x9000 => {}
263 0x6985 | 0x5501 => return Err(Error::UserRefused),
264 0x6a84 | 0x5102 => return Err(Error::App("not enough space on the device".into())),
265 _ => {
266 return Err(Error::App(format!(
267 "APDU {} of {total} failed: {}",
268 i + 1,
269 r.sw
270 )));
271 }
272 }
273 on_progress(i + 1, total);
274 }
275 Ok(())
276 }
277
278 pub async fn install_language_script(
281 &self,
282 script: &str,
283 on_progress: &mut dyn FnMut(usize, usize),
284 ) -> Result<()> {
285 for installed in self.list_language_packs().await? {
286 self.delete_language_pack(installed.id as u8).await?;
287 }
288 self.run_apdu_script(script, on_progress).await
289 }
290
291 pub async fn delete_language_pack(&self, language: u8) -> Result<()> {
293 self.check(self.exchange(&os::delete_language_pack(language)).await?)
294 .map(|_| ())
295 }
296
297 pub async fn endorsement_info(&self, slot: EndorsementSlot) -> Result<EndorsementInfo> {
299 let r = self.exchange(&os::endorsement_info(slot)).await?;
300 if !r.sw.is_ok() && !r.sw.is_refused() && r.sw != StatusWord::LOCKED_DEVICE {
301 return Err(Error::NotFound(format!(
302 "endorsement slot {} is not set up ({})",
303 slot as u8, r.sw
304 )));
305 }
306 EndorsementInfo::parse(&self.check(r)?)
307 }
308
309 pub async fn endorse_start(&self, slot: EndorsementSlot) -> Result<(Vec<u8>, Vec<u8>)> {
313 let d = self.check(self.exchange(&os::endorse_set_start(slot)).await?)?;
314 if d.len() < 65 {
315 return Err(Error::InvalidResponse("short endorsement key".into()));
316 }
317 Ok((d[..65].to_vec(), d[65..].to_vec()))
318 }
319
320 pub async fn endorse_commit(&self, certificate: &[u8]) -> Result<()> {
323 self.check(self.exchange(&os::endorse_set_commit(certificate)).await?)
324 .map(|_| ())
325 }
326
327 pub async fn onboard(
330 &self,
331 identity: OnboardIdentity,
332 pin: &str,
333 prefix: &str,
334 passphrase: &str,
335 words: &str,
336 ) -> Result<()> {
337 self.check(
338 self.exchange(&os::onboard(identity, pin, prefix, passphrase, words)?)
339 .await?,
340 )
341 .map(|_| ())
342 }
343
344 pub async fn factory_get_device_public_key(&self) -> Result<Vec<u8>> {
347 self.check(self.exchange(&os::GET_DEVICE_PUBLIC_KEY).await?)
348 }
349
350 pub async fn factory_test(&self, p1: u8, p2: u8, data: &[u8]) -> Result<Vec<u8>> {
352 self.check(self.exchange(&os::factory_test(p1, p2, data)).await?)
353 }
354
355 pub async fn factory_get_cxport(&self) -> Result<Vec<u8>> {
357 self.check(self.exchange(&os::GET_CXPORT).await?)
358 }
359
360 pub async fn factory_set_certificate(&self, p2: u8, data: &[u8]) -> Result<Vec<u8>> {
362 self.check(
363 self.exchange(&Apdu::with_data(0xe0, 0x41, 0x00, p2, data.to_vec()))
364 .await?,
365 )
366 }
367
368 pub async fn factory_set_cxport(&self, data: &[u8]) -> Result<Vec<u8>> {
370 self.check(
371 self.exchange(&Apdu::with_data(0xe0, 0xc4, 0x00, 0x00, data.to_vec()))
372 .await?,
373 )
374 }
375
376 pub async fn get_battery_raw(&self, kind: BatteryStatusKind) -> Result<Vec<u8>> {
378 self.send(&os::get_battery_status(kind)).await
379 }
380
381 pub async fn get_battery_status(&self) -> Result<BatteryStatus> {
383 let mut st = BatteryStatus::default();
384 let p = self.get_battery_raw(BatteryStatusKind::Percentage).await?;
385 st.percentage = p.first().copied().filter(|v| *v <= 100);
386 if let Ok(v) = self.get_battery_raw(BatteryStatusKind::Voltage).await
387 && v.len() >= 2
388 {
389 st.voltage_mv = Some(u16::from_be_bytes([v[0], v[1]]));
390 }
391 if let Ok(v) = self.get_battery_raw(BatteryStatusKind::Temperature).await {
392 st.temperature_c = v.first().map(|b| *b as i8);
393 }
394 if let Ok(v) = self.get_battery_raw(BatteryStatusKind::Current).await {
395 st.current_ma = v.first().map(|b| *b as i8);
396 }
397 if let Ok(v) = self.get_battery_raw(BatteryStatusKind::Flags).await
398 && v.len() >= 4
399 {
400 let f = u32::from_be_bytes([v[0], v[1], v[2], v[3]]);
401 st.raw_flags = Some(f);
402 st.charging = f & 0x1 != 0;
403 st.usb_connected = f & 0x2 != 0;
404 st.ble_connected = f & 0x4 != 0;
405 }
406 Ok(st)
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413 use crate::transport::mock::MockTransport;
414
415 fn block_on<F: core::future::Future>(f: F) -> F::Output {
416 tokio::runtime::Builder::new_current_thread()
417 .build()
418 .unwrap()
419 .block_on(f)
420 }
421
422 #[test]
423 fn list_apps_paging() {
424 let mut page = String::from("01");
425 page += &format!("{:02x}", 70 + 3);
426 page += "00100000";
427 page += &"aa".repeat(32);
428 page += &"bb".repeat(32);
429 page += "03657468";
430 let t = MockTransport::new()
431 .expect("e0de000000", &format!("{page}9000"))
432 .expect("e0df000000", "9000");
433 let d = Device::new(t);
434 let apps = block_on(d.list_apps()).unwrap();
435 assert_eq!(apps.len(), 1);
436 assert_eq!(apps[0].name, "eth");
437 assert!(d.transport().is_done());
438 }
439
440 #[test]
441 fn dashboard_detection() {
442 let t = MockTransport::new().expect("b001000000", "6e01");
443 assert!(block_on(Device::new(t).is_on_dashboard()).unwrap());
444 }
445}