use crate::apdu::{Apdu, Reader};
use crate::error::{Error, Result};
use crate::model::{DeviceModel, version_gte};
pub const GET_VERSION: Apdu = Apdu::new(0xe0, 0x01, 0x00, 0x00);
pub const GET_APP_AND_VERSION: Apdu = Apdu::new(0xb0, 0x01, 0x00, 0x00);
pub const QUIT_APP: Apdu = Apdu::new(0xb0, 0xa7, 0x00, 0x00);
pub const LIST_APPS: Apdu = Apdu::new(0xe0, 0xde, 0x00, 0x00);
pub const LIST_APPS_CONTINUE: Apdu = Apdu::new(0xe0, 0xdf, 0x00, 0x00);
pub const GET_DEVICE_NAME: Apdu = Apdu::new(0xe0, 0xd2, 0x00, 0x00);
pub fn open_app(name: &str) -> Apdu {
Apdu::with_data(0xe0, 0xd8, 0x00, 0x00, name.as_bytes().to_vec())
}
pub fn set_device_name(name: &str) -> Apdu {
Apdu::with_data(0xe0, 0xd4, 0x00, 0x00, name.as_bytes().to_vec())
}
pub fn get_battery_status(kind: BatteryStatusKind) -> Apdu {
Apdu::new(0xe0, 0x10, 0x00, kind as u8)
}
pub fn is_dashboard_name(name: &str) -> bool {
name == "BOLOS" || name == "OLOS\0"
}
pub fn provider_id_from_name(name: Option<&str>) -> u32 {
match name {
Some("das") => 2,
Some("club") => 3,
Some("shitcoins") => 4,
Some("ee") => 5,
_ => 1,
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct OsVersion {
pub is_bootloader: bool,
pub is_osu: bool,
pub target_id: u32,
pub se_target_id: Option<u32>,
pub mcu_target_id: Option<u32>,
pub raw_version: String,
pub se_version: String,
#[cfg_attr(feature = "serde", serde(serialize_with = "ser_hex"))]
pub se_flags: Vec<u8>,
pub mcu_seph_version: String,
pub mcu_bootloader_version: String,
pub hw_version: String,
pub language_id: Option<u32>,
pub recover_state: Option<u32>,
pub flags: SeFlags,
}
#[cfg(feature = "serde")]
fn ser_hex<S: serde::Serializer>(v: &[u8], s: S) -> core::result::Result<S::Ok, S::Error> {
s.serialize_str(&hex::encode(v))
}
impl OsVersion {
pub fn parse(data: &[u8]) -> Result<Self> {
let mut r = Reader::new(data);
let target_id = r.u32()?;
let mut raw_version = String::from_utf8_lossy(r.opt_lv()?.unwrap_or_default()).into_owned();
let mut se_flags = r.opt_lv()?.unwrap_or_default().to_vec();
if raw_version.is_empty() {
raw_version = "0.0.0".into();
se_flags.clear();
}
let is_bootloader = target_id & 0xf000_0000 != 0x3000_0000;
let is_osu = raw_version.contains("-osu");
let model = DeviceModel::from_target_id(target_id);
let mut v = OsVersion {
is_bootloader,
is_osu,
target_id,
se_target_id: None,
mcu_target_id: None,
raw_version: raw_version.clone(),
se_version: String::new(),
flags: SeFlags::parse(&se_flags),
se_flags,
mcu_seph_version: String::new(),
mcu_bootloader_version: String::new(),
hw_version: String::new(),
language_id: None,
recover_state: None,
};
let str_of = |b: &[u8]| {
String::from_utf8_lossy(b)
.trim_end_matches('\0')
.to_string()
};
let int_of = |b: &[u8]| b.iter().fold(0u32, |acc, x| (acc << 8) | *x as u32);
if is_bootloader {
v.mcu_bootloader_version = raw_version;
v.mcu_target_id = Some(target_id);
if let Some(se) = r.opt_lv()? {
if se.len() >= 5 {
v.se_version = str_of(se);
v.se_target_id = r.opt_lv()?.map(int_of);
} else {
v.se_target_id = Some(int_of(se));
}
}
} else {
v.se_version = raw_version;
v.se_target_id = Some(target_id);
v.mcu_seph_version = r.opt_lv()?.map(str_of).unwrap_or_default();
let se = v.se_version.as_str();
let bl_supported = match model {
Some(DeviceModel::NanoS | DeviceModel::NanoX) => version_gte(se, (2, 0, 0)),
_ => true,
};
if bl_supported {
v.mcu_bootloader_version = r.opt_lv()?.map(str_of).unwrap_or_default();
}
let hw_supported =
matches!(model, Some(DeviceModel::NanoX)) && version_gte(se, (2, 0, 0));
v.hw_version = if hw_supported {
r.opt_lv()?.map(hex::encode).unwrap_or_default()
} else {
"00".into()
};
let lang_supported = match model {
Some(DeviceModel::NanoS) => false,
Some(DeviceModel::NanoSPlus) => version_gte(se, (1, 1, 0)),
Some(DeviceModel::NanoX) => version_gte(se, (2, 1, 0)),
_ => true,
};
if lang_supported {
v.language_id = r.opt_lv()?.map(int_of);
}
let recover_supported = match model {
Some(DeviceModel::NanoS) => false,
Some(DeviceModel::NanoSPlus) => version_gte(se, (1, 1, 2)),
Some(DeviceModel::NanoX) => version_gte(se, (2, 2, 3)),
Some(DeviceModel::Stax) => version_gte(se, (1, 4, 0)),
Some(DeviceModel::Flex) => version_gte(se, (1, 0, 1)),
_ => true,
};
if recover_supported {
v.recover_state = r.opt_lv()?.map(int_of);
}
}
Ok(v)
}
pub fn model(&self) -> Option<DeviceModel> {
DeviceModel::from_target_id(self.target_id)
.or_else(|| self.se_target_id.and_then(DeviceModel::from_target_id))
}
pub fn version(&self) -> String {
self.raw_version.replace("-osu", "")
}
pub fn maj_min(&self) -> String {
self.raw_version
.split('-')
.next()
.unwrap_or_default()
.to_string()
}
pub fn provider_name(&self) -> Option<String> {
let suffix = self.raw_version.split_once('-')?.1;
match suffix {
"das" | "club" | "shitcoins" | "ee" => Some(suffix.to_string()),
_ => None,
}
}
pub fn provider_id(&self) -> u32 {
provider_id_from_name(self.provider_name().as_deref())
}
pub fn mcu_version(&self) -> &str {
&self.mcu_seph_version
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct SeFlags {
pub is_pin_validated: bool,
pub has_mcu_serial_number: bool,
pub has_valid_certificate: bool,
pub is_custom_authority_connection_allowed: bool,
pub is_secure_connection_allowed: bool,
pub is_onboarded: bool,
pub is_mcu_code_signed: bool,
pub is_in_recovery_mode: bool,
pub has_endorsement_slot1: bool,
pub has_endorsement_slot2: bool,
pub seed_word_count: Option<u8>,
pub current_word_index: u8,
pub onboarding_state: u8,
}
impl SeFlags {
pub fn parse(flags: &[u8]) -> Self {
let mut f = [0u8; 4];
let n = flags.len().min(4);
f[..n].copy_from_slice(&flags[..n]);
let bit = |b: u8, n: u8| (b >> (8 - n)) & 1 == 1;
let words = (f[2] >> 5) & 0x03;
SeFlags {
is_pin_validated: bit(f[0], 1),
has_mcu_serial_number: bit(f[0], 2),
has_valid_certificate: bit(f[0], 3),
is_custom_authority_connection_allowed: bit(f[0], 4),
is_secure_connection_allowed: bit(f[0], 5),
is_onboarded: bit(f[0], 6),
is_mcu_code_signed: bit(f[0], 7),
is_in_recovery_mode: bit(f[0], 8),
has_endorsement_slot1: f[1] & 1 != 0,
has_endorsement_slot2: f[1] & 2 != 0,
seed_word_count: [Some(24), Some(18), Some(12), None][words as usize],
current_word_index: f[2] & 0x1f,
onboarding_state: f[3],
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct AppAndVersion {
pub name: String,
pub version: String,
pub flags: Option<u32>,
}
impl AppAndVersion {
pub fn parse(data: &[u8]) -> Result<Self> {
let mut r = Reader::new(data);
if r.u8()? != 1 {
return Err(Error::InvalidResponse(
"unknown GET_APP_AND_VERSION format".into(),
));
}
let name = r.lv_string()?;
let version = r.lv_string()?;
let flags = r
.opt_lv()?
.map(|b| b.iter().fold(0u32, |acc, x| (acc << 8) | *x as u32));
Ok(Self {
name,
version,
flags,
})
}
pub fn is_dashboard(&self) -> bool {
is_dashboard_name(&self.name)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct InstalledApp {
pub name: String,
pub blocks: u16,
pub flags: u16,
pub hash_code_data: String,
pub hash: String,
}
pub fn parse_list_apps(data: &[u8]) -> Result<Vec<InstalledApp>> {
if data.is_empty() {
return Ok(Vec::new());
}
let mut r = Reader::new(data);
if r.u8()? != 1 {
return Err(Error::InvalidResponse("unknown LIST_APPS format".into()));
}
let mut apps = Vec::new();
while !r.is_empty() {
let _entry_len = r.u8()?;
let blocks = r.u16()?;
let flags = r.u16()?;
let hash_code_data = hex::encode(r.bytes(32)?);
let hash = hex::encode(r.bytes(32)?);
let name = r.lv_string()?;
apps.push(InstalledApp {
name,
blocks,
flags,
hash_code_data,
hash,
});
}
Ok(apps)
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(u8)]
pub enum BatteryStatusKind {
Percentage = 0,
Voltage = 1,
Temperature = 2,
Current = 3,
Flags = 4,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "camelCase"))]
pub struct BatteryStatus {
pub percentage: Option<u8>,
pub voltage_mv: Option<u16>,
pub temperature_c: Option<i8>,
pub current_ma: Option<i8>,
pub charging: bool,
pub usb_connected: bool,
pub ble_connected: bool,
pub raw_flags: Option<u32>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_version_nanosp() {
let mut d = vec![0x33, 0x10, 0x00, 0x04];
d.push(5);
d.extend_from_slice(b"1.1.1");
d.extend_from_slice(&[4, 0xae, 0, 0, 0x0b]);
d.push(4);
d.extend_from_slice(b"5.24");
d.push(4);
d.extend_from_slice(b"0.11");
d.extend_from_slice(&[1, 0]);
let v = OsVersion::parse(&d).unwrap();
assert_eq!(v.model(), Some(DeviceModel::NanoSPlus));
assert_eq!(v.se_version, "1.1.1");
assert_eq!(v.mcu_seph_version, "5.24");
assert_eq!(v.mcu_bootloader_version, "0.11");
assert_eq!(v.language_id, Some(0));
assert!(v.flags.is_pin_validated);
assert!(v.flags.is_secure_connection_allowed);
assert!(!v.is_bootloader);
assert_eq!(v.recover_state, None);
}
#[test]
fn parse_app_version() {
let d = [
1, 5, b'B', b'O', b'L', b'O', b'S', 5, b'1', b'.', b'4', b'.', b'1',
];
let a = AppAndVersion::parse(&d).unwrap();
assert!(a.is_dashboard());
assert_eq!(a.version, "1.4.1");
}
#[test]
fn parse_apps() {
let mut d = vec![1u8];
let name = b"Bitcoin";
d.push(70 + name.len() as u8);
d.extend_from_slice(&[0x00, 0x20, 0x00, 0x08]);
d.extend_from_slice(&[0xaa; 32]);
d.extend_from_slice(&[0xbb; 32]);
d.push(name.len() as u8);
d.extend_from_slice(name);
let apps = parse_list_apps(&d).unwrap();
assert_eq!(apps.len(), 1);
assert_eq!(apps[0].name, "Bitcoin");
assert_eq!(apps[0].blocks, 0x20);
assert_eq!(apps[0].hash, "bb".repeat(32));
}
}