summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.toml21
-rw-r--r--examples/list.rs21
-rw-r--r--src/crc32.rs84
-rw-r--r--src/devices.rs62
-rw-r--r--src/devices/pro.rs99
-rw-r--r--src/features.rs20
-rw-r--r--src/hid.rs187
-rw-r--r--src/lib.rs101
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()
+ }
+}