diff options
author | Robin Krahl <robin.krahl@ireas.org> | 2021-03-27 16:10:53 +0100 |
---|---|---|
committer | Robin Krahl <robin.krahl@ireas.org> | 2021-03-27 16:10:53 +0100 |
commit | 434e61c231e142b6f5f8d81eb25f4ef97686d85a (patch) | |
tree | db884ec48a50772e29b99cc42ff6b0cca4a79228 | |
download | nitrokey-hid-434e61c231e142b6f5f8d81eb25f4ef97686d85a.tar.gz nitrokey-hid-434e61c231e142b6f5f8d81eb25f4ef97686d85a.tar.bz2 |
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.toml | 21 | ||||
-rw-r--r-- | examples/list.rs | 21 | ||||
-rw-r--r-- | src/crc32.rs | 84 | ||||
-rw-r--r-- | src/devices.rs | 62 | ||||
-rw-r--r-- | src/devices/pro.rs | 99 | ||||
-rw-r--r-- | src/features.rs | 20 | ||||
-rw-r--r-- | src/hid.rs | 187 | ||||
-rw-r--r-- | src/lib.rs | 101 |
9 files changed, 597 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..e096d97 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "nitrokey-hid" +version = "0.1.0" +authors = ["Robin Krahl <robin.krahl@ireas.org>"] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde-big-array = "0.3" +ssmarshal = "1" +static_assertions = "1" + +[dependencies.hidapi] +version = "1.2.5" +default-features = false +features = ["linux-shared-libusb"] + +[dependencies.serde] +version = "1" +features = ["derive"] diff --git a/examples/list.rs b/examples/list.rs new file mode 100644 index 0000000..ee59ceb --- /dev/null +++ b/examples/list.rs @@ -0,0 +1,21 @@ +use nitrokey_hid::features::Basic; + +fn main() -> Result<(), nitrokey_hid::Error> { + let device = nitrokey_hid::connect()?; + let status = device.get_status()?; + println!( + r#"Status: + model: {model:?} + serial number: 0x{id:08x} + firmware version: {fwv_maj}.{fwv_min} + user retry count: {urc} + admin retry count: {arc}"#, + model = device.get_model(), + id = status.serial_number, + fwv_maj = status.firmware_version_major, + fwv_min = status.firmware_version_minor, + urc = device.get_user_retry_count()?, + arc = device.get_admin_retry_count()?, + ); + Ok(()) +} diff --git a/src/crc32.rs b/src/crc32.rs new file mode 100644 index 0000000..6b3f9d7 --- /dev/null +++ b/src/crc32.rs @@ -0,0 +1,84 @@ +// https://github.com/d-e-s-o/nitrocli/blob/ac29709a644682c61a5a28d2a23f8887174fcc31/nitrocli/src/crc32.rs +// +// ************************************************************************* +// * Copyright (C) 2017 Daniel Mueller (deso@posteo.net) * +// * * +// * This program is free software: you can redistribute it and/or modify * +// * it under the terms of the GNU General Public License as published by * +// * the Free Software Foundation, either version 3 of the License, or * +// * (at your option) any later version. * +// * * +// * This program is distributed in the hope that it will be useful, * +// * but WITHOUT ANY WARRANTY; without even the implied warranty of * +// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * +// * GNU General Public License for more details. * +// * * +// * You should have received a copy of the GNU General Public License * +// * along with this program. If not, see <http://www.gnu.org/licenses/>. * +// ************************************************************************* + +/// Polynomial used in STM32. +const CRC32_POLYNOMIAL: u32 = 0x04c11db7; + +fn crc32(mut crc: u32, data: u32) -> u32 { + crc = crc ^ data; + + for _ in 0..32 { + if crc & 0x80000000 != 0 { + crc = (crc << 1) ^ CRC32_POLYNOMIAL; + } else { + crc = crc << 1; + } + } + return crc; +} + +/// Retrieve a u32 slice of the 'data' part. +/// +/// Note that the size of the supplied data has to be a multiple of 4 +/// bytes. +fn as_slice_u32(data: &[u8]) -> &[u32] { + assert!(data.len() % ::std::mem::size_of::<u32>() == 0); + + unsafe { + let ptr = data.as_ptr() as *const u32; + let len = data.len() / ::std::mem::size_of::<u32>(); + return ::std::slice::from_raw_parts(ptr, len); + } +} + +/// Calculate the CRC of a byte slice. +pub fn crc(data: &[u8]) -> u32 { + let mut crc = 0xffffffff; + + for value in as_slice_u32(data) { + crc = crc32(crc, *value); + } + + return crc; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_crc32() { + let mut crc = 0; + + // The expected values were computed with the original function. + crc = crc32(crc, 0xdeadbeef); + assert_eq!(crc, 0x46dec763); + + crc = crc32(crc, 42); + assert_eq!(crc, 0x7e579b45); + } + + #[test] + fn test_crc() { + let data = &"thisisatextthatistobecrced..".to_string().into_bytes(); + let crc = crc(data); + + assert_eq!(crc, 0x469db4ee); + } +} diff --git a/src/devices.rs b/src/devices.rs new file mode 100644 index 0000000..a40207b --- /dev/null +++ b/src/devices.rs @@ -0,0 +1,62 @@ +use crate::features; +use crate::{Error, Model}; + +mod pro; + +pub use pro::Pro; + +#[derive(Debug)] +pub enum Device { + Pro(Pro), +} + +impl Device { + fn as_basic(&self) -> &dyn features::Basic { + match self { + Device::Pro(pro) => pro, + } + } +} + +impl RawDevice for Device { + fn execute<T, U>(&self, command_id: impl Into<u8>, data: T) -> Result<U, Error> + where + T: serde::Serialize, + U: serde::de::DeserializeOwned + { + match self { + Device::Pro(pro) => pro.execute(command_id, data), + } + } +} + +impl features::Basic for Device { + fn get_model(&self) -> Model { + self.as_basic().get_model() + } + + fn get_status(&self) -> Result<features::Status, Error> { + self.as_basic().get_status() + } + + fn get_user_retry_count(&self) -> Result<u8, Error> { + self.as_basic().get_user_retry_count() + } + + fn get_admin_retry_count(&self) -> Result<u8, Error> { + self.as_basic().get_admin_retry_count() + } +} + +impl From<Pro> for Device { + fn from(pro: Pro) -> Device { + Device::Pro(pro) + } +} + +pub(crate) trait RawDevice { + fn execute<T, U>(&self, command_id: impl Into<u8>, data: T) -> Result<U, Error> + where + T: serde::Serialize, + U: serde::de::DeserializeOwned; +} diff --git a/src/devices/pro.rs b/src/devices/pro.rs new file mode 100644 index 0000000..f986f6b --- /dev/null +++ b/src/devices/pro.rs @@ -0,0 +1,99 @@ +use std::fmt; + +use crate::{devices, hid, features, DeviceInfo, Error, Model}; +use crate::devices::RawDevice as _; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u8)] +enum Command { + GetStatus = 0x00, + GetPasswordRetryCount = 0x09, + GetUserPasswordRetryCount = 0x0F, +} + +impl From<Command> for u8 { + fn from(command: Command) -> u8 { + command as u8 + } +} + +pub struct Pro { + hid_device: hidapi::HidDevice, + path: std::ffi::CString, +} + +impl Pro { + pub(crate) fn new(hid_device: hidapi::HidDevice, device_info: &DeviceInfo<'_>) -> Self { + Self { + hid_device, + path: device_info.device_info.path().to_owned(), + } + } +} + +impl features::Basic for Pro { + fn get_model(&self) -> Model { + Model::Pro + } + + fn get_status(&self) -> Result<features::Status, Error> { + self.execute(Command::GetStatus, ()) + } + + fn get_user_retry_count(&self) -> Result<u8, Error> { + self.execute(Command::GetPasswordRetryCount, ()) + } + + fn get_admin_retry_count(&self) -> Result<u8, Error> { + self.execute(Command::GetUserPasswordRetryCount, ()) + } +} + +impl devices::RawDevice for Pro { + fn execute<T, U>(&self, command_id: impl Into<u8>, data: T) -> Result<U, Error> + where + T: serde::Serialize, + U: serde::de::DeserializeOwned, + { + use hid::HidDeviceExt as _; + + let command_id = command_id.into(); + let crc = self.hid_device.send(&hid::Request::new(command_id, data))?; + + let mut response = self.hid_device.receive()?; + let mut retry_count = 0; + while response.device_status == hid::DeviceStatus::Busy && retry_count < 3 { + std::thread::sleep(std::time::Duration::from_millis(100)); + response = self.hid_device.receive()?; + retry_count += 1; + } + + if response.device_status != hid::DeviceStatus::Ok { + return Err(format!("Got device status {:?}", response.device_status).into()); + } + if response.command_id != command_id { + return Err(format!( + "Expected command ID {}, got {}", + command_id, response.command_id + ) + .into()); + } + if response.command_status != hid::CommandStatus::Ok { + return Err(format!("Got command status {:?}", response.command_status).into()); + } + if response.last_crc() != crc { + return Err(format!("Expected last_crc {}, got {}", crc, response.last_crc()).into()); + } + + Ok(response.data) + } +} + +impl fmt::Debug for Pro { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("Pro") + .field("path", &self.path) + .finish() + } +} + diff --git a/src/features.rs b/src/features.rs new file mode 100644 index 0000000..1ec5dff --- /dev/null +++ b/src/features.rs @@ -0,0 +1,20 @@ +use crate::{Error, Model}; + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)] +pub struct Status { + pub firmware_version_minor: u8, + pub firmware_version_major: u8, + pub serial_number: u32, + pub config_num_lock: u8, + pub config_caps_lock: u8, + pub config_scroll_lock: u8, + pub config_enable_user_password: u8, + _unused: u8, +} + +pub trait Basic { + fn get_model(&self) -> Model; + fn get_status(&self) -> Result<Status, Error>; + fn get_user_retry_count(&self) -> Result<u8, Error>; + fn get_admin_retry_count(&self) -> Result<u8, Error>; +} diff --git a/src/hid.rs b/src/hid.rs new file mode 100644 index 0000000..291216c --- /dev/null +++ b/src/hid.rs @@ -0,0 +1,187 @@ +use std::convert; +use std::convert::TryFrom as _; + +use crate::crc32; +use crate::Error; + +pub const REPORT_LEN: usize = 65; +pub const REPORT_PAYLOAD_LEN: usize = REPORT_LEN - 5; +pub const MAX_REQUEST_DATA_LEN: usize = REPORT_PAYLOAD_LEN - 1; +pub const MAX_RESPONSE_DATA_LEN: usize = REPORT_PAYLOAD_LEN - 7; + +serde_big_array::big_array! { BigArray; REPORT_PAYLOAD_LEN } + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, serde::Serialize)] +struct Report { + report_id: u8, + #[serde(with = "BigArray")] + payload: [u8; REPORT_PAYLOAD_LEN], + crc: [u8; 4], +} + +static_assertions::assert_eq_size!([u8; REPORT_LEN], Report); + +impl Report { + fn new(report_id: u8, payload: [u8; REPORT_PAYLOAD_LEN]) -> Report { + let crc = crc32::crc(&payload); + Report { + report_id, + payload, + crc: crc.to_le_bytes(), + } + } + + fn from_bytes(data: [u8; REPORT_LEN]) -> Result<Report, Error> { + let (report, num_bytes): (Report, _) = ssmarshal::deserialize(&data)?; + if num_bytes != REPORT_LEN { + return Err(format!("Only {} of {} report bytes read", num_bytes, REPORT_LEN).into()); + } + Ok(report) + } + + fn to_bytes(&self) -> Result<[u8; REPORT_LEN], Error> { + let mut buf = [0; REPORT_LEN]; + let num_bytes = ssmarshal::serialize(&mut buf, self)?; + if num_bytes != REPORT_LEN { + return Err(format!("Invalid report length: {}", num_bytes).into()); + } + Ok(buf) + } + + fn crc(&self) -> u32 { + u32::from_le_bytes(self.crc) + } + + pub fn check_crc(&self) -> Result<(), Error> { + let crc = crc32::crc(&self.payload); + if self.crc() != crc { + Err(format!( + "Warning: Report has wrong CRC: got {:x}, expected {:x}", + self.crc(), + crc + ) + .into()) + } else { + Ok(()) + } + } +} + +impl Default for Report { + fn default() -> Report { + Report::new(0, [0; REPORT_PAYLOAD_LEN]) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct Request<T> { + pub command_id: u8, + pub data: T, +} + +static_assertions::assert_eq_size!([u8; REPORT_PAYLOAD_LEN - MAX_REQUEST_DATA_LEN], Request<()>,); + +impl<T: serde::Serialize> Request<T> { + pub fn new(command_id: u8, data: T) -> Request<T> { + Request { command_id, data } + } +} + +impl<'a, T: serde::Serialize> convert::TryFrom<&'a Request<T>> for Report { + type Error = Error; + + fn try_from(request: &'a Request<T>) -> Result<Report, Error> { + let mut payload = [0; REPORT_PAYLOAD_LEN]; + let num_bytes = ssmarshal::serialize(&mut payload, &request)?; + if num_bytes > REPORT_PAYLOAD_LEN { + return Err(format!("Invalid request length: {}", num_bytes).into()); + } + Ok(Report::new(0, payload)) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)] +#[repr(u8)] +pub enum DeviceStatus { + Ok, + Busy, + Error, + ReceivedReport, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)] +#[repr(u8)] +pub enum CommandStatus { + Ok, + WrongCrc, + WrongSlot, + SlotNotProgrammed, + WrongPassword, + NotAuthorized, + TimestampWarning, + NoNameError, + NotSupported, + UnknownCommand, + AesDecFailed, +} + +#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)] +pub struct Response<T> { + pub device_status: DeviceStatus, + pub command_id: u8, + pub last_crc: [u8; 4], + pub command_status: CommandStatus, + pub data: T, +} + +static_assertions::assert_eq_size!( + [u8; REPORT_PAYLOAD_LEN - MAX_RESPONSE_DATA_LEN], + Response<()>, +); + +impl<T: serde::de::DeserializeOwned> Response<T> { + pub fn last_crc(&self) -> u32 { + u32::from_le_bytes(self.last_crc) + } +} + +impl<'a, T: serde::de::DeserializeOwned> convert::TryFrom<&'a Report> for Response<T> { + type Error = Error; + + fn try_from(report: &'a Report) -> Result<Response<T>, Error> { + let (response, _) = ssmarshal::deserialize(&report.payload)?; + Ok(response) + } +} + +pub trait HidDeviceExt { + fn send<T: serde::Serialize>(&self, request: &Request<T>) -> Result<u32, Error>; + fn receive<T: serde::de::DeserializeOwned>(&self) -> Result<Response<T>, Error>; +} + +impl HidDeviceExt for hidapi::HidDevice { + fn send<T: serde::Serialize>(&self, request: &Request<T>) -> Result<u32, Error> { + let report = Report::try_from(request)?; + let data = report.to_bytes()?; + self.send_feature_report(&data)?; + Ok(report.crc()) + } + + fn receive<T: serde::de::DeserializeOwned>(&self) -> Result<Response<T>, Error> { + let mut buf = [0; REPORT_LEN]; + let n = self.get_feature_report(&mut buf)?; + if n != REPORT_LEN { + return Err(format!("Invalid report length: {}", n).into()); + } + let report = Report::from_bytes(buf)?; + if report.report_id != 0 { + return Err(format!("Invalid report ID: {}", report.report_id).into()); + } + let response = Response::try_from(&report)?; + // TODO: is the CRC set for all other status variants? + if response.device_status != DeviceStatus::Busy { + report.check_crc()?; + } + Ok(response) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..a26f02f --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,101 @@ +mod crc32; +mod hid; + +pub mod devices; +pub mod features; + +use std::fmt; + +pub type Error = Box<dyn std::error::Error>; + +const VID_NITROKEY: u16 = 0x20a0; +const PID_NITROKEY_PRO: u16 = 0x4108; +const PID_NITROKEY_STORAGE: u16 = 0x4109; + +pub fn connect() -> Result<devices::Device, Error> { + let manager = Manager::new()?; + if let Some(device) = manager.devices().first() { + device.connect() + } else { + Err("No device connected".into()) + } +} + +pub fn connect_model(model: Model) -> Result<devices::Device, Error> { + let manager = Manager::new()?; + if let Some(device) = manager.devices().iter().filter(|i| i.model == model).next() { + device.connect() + } else { + Err(format!("No {:?} device connected", model).into()) + } +} + +pub struct Manager { + hidapi: hidapi::HidApi, +} + +impl Manager { + pub fn new() -> Result<Self, Error> { + let hidapi = hidapi::HidApi::new()?; + Ok(Manager { hidapi }) + } + + pub fn devices(&self) -> Vec<DeviceInfo<'_>> { + self.hidapi + .device_list() + .flat_map(|device_info| { + Model::from_vid_pid(device_info.vendor_id(), device_info.product_id()).map( + |model| DeviceInfo { + hidapi: &self.hidapi, + device_info, + model, + }, + ) + }) + .collect() + } +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] +pub enum Model { + Pro, + Storage, +} + +impl Model { + fn from_vid_pid(vid: u16, pid: u16) -> Option<Model> { + match vid { + VID_NITROKEY => match pid { + PID_NITROKEY_PRO => Some(Model::Pro), + PID_NITROKEY_STORAGE => Some(Model::Storage), + _ => None, + }, + _ => None, + } + } +} + +pub struct DeviceInfo<'a> { + hidapi: &'a hidapi::HidApi, + device_info: &'a hidapi::DeviceInfo, + model: Model, +} + +impl<'a> DeviceInfo<'a> { + pub fn connect(&self) -> Result<devices::Device, Error> { + let hid_device = self.device_info.open_device(self.hidapi)?; + match self.model { + Model::Pro => Ok(devices::Pro::new(hid_device, self).into()), + Model::Storage => unimplemented!(), + } + } +} + +impl<'a> fmt::Debug for DeviceInfo<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DeviceInfo") + .field("model", &self.model) + .field("path", &self.device_info.path()) + .finish() + } +} |