diff options
-rw-r--r-- | nitrocli/CHANGELOG.md | 4 | ||||
-rw-r--r-- | nitrocli/src/crc32.rs | 88 | ||||
-rw-r--r-- | nitrocli/src/error.rs | 9 | ||||
-rw-r--r-- | nitrocli/src/main.rs | 237 | ||||
-rw-r--r-- | nitrocli/src/nitrokey.rs | 256 |
5 files changed, 27 insertions, 567 deletions
diff --git a/nitrocli/CHANGELOG.md b/nitrocli/CHANGELOG.md index 7ff193a..c8985f3 100644 --- a/nitrocli/CHANGELOG.md +++ b/nitrocli/CHANGELOG.md @@ -1,7 +1,7 @@ Unreleased ---------- -- Use the `nitrokey` crate for the `open` and `close` commands instead - of directly communicating with the Nitrokey device +- Use the `nitrokey` crate for the `open`, `close`, and `status` + commands instead of directly communicating with the Nitrokey device - Added `nitrokey` version `0.2.1` as a direct dependency and `nitrokey-sys` version `3.4.1` as well as `rand` version `0.4.3` as indirect dependencies diff --git a/nitrocli/src/crc32.rs b/nitrocli/src/crc32.rs deleted file mode 100644 index 8b85b49..0000000 --- a/nitrocli/src/crc32.rs +++ /dev/null @@ -1,88 +0,0 @@ -// crc32.rs - -// ************************************************************************* -// * Copyright (C) 2017-2018 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 = 0x04c1_1db7; - - -fn crc32(mut crc: u32, data: u32) -> u32 { - crc ^= data; - - for _ in 0..32 { - if crc & 0x8000_0000 != 0 { - crc = (crc << 1) ^ CRC32_POLYNOMIAL; - } else { - crc <<= 1; - } - } - 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>(); - ::std::slice::from_raw_parts(ptr, len) - } -} - - -/// Calculate the CRC of a byte slice. -pub fn crc(data: &[u8]) -> u32 { - let mut crc = 0xffff_ffff; - let data = as_slice_u32(data); - - for byte in data { - crc = crc32(crc, *byte); - } - 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/nitrocli/src/error.rs b/nitrocli/src/error.rs index 5d6b266..80d9d92 100644 --- a/nitrocli/src/error.rs +++ b/nitrocli/src/error.rs @@ -26,20 +26,12 @@ use libhid; #[derive(Debug)] pub enum Error { - HidError(libhid::Error), IoError(io::Error), Utf8Error(string::FromUtf8Error), Error(String), } -impl From<libhid::Error> for Error { - fn from(e: libhid::Error) -> Error { - Error::HidError(e) - } -} - - impl From<io::Error> for Error { fn from(e: io::Error) -> Error { Error::IoError(e) @@ -57,7 +49,6 @@ impl From<string::FromUtf8Error> for Error { impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { - Error::HidError(ref e) => write!(f, "hidapi error: {}", e), Error::Utf8Error(_) => write!(f, "Encountered UTF-8 conversion error"), Error::IoError(ref e) => write!(f, "IO error: {}", e.get_ref().unwrap()), Error::Error(ref e) => write!(f, "{}", e), diff --git a/nitrocli/src/main.rs b/nitrocli/src/main.rs index 714c573..5ec68a2 100644 --- a/nitrocli/src/main.rs +++ b/nitrocli/src/main.rs @@ -68,29 +68,19 @@ //! Nitrocli is a program providing a command line interface to certain //! commands of the Nitrokey Storage device. -mod crc32; mod error; -mod nitrokey; mod pinentry; -use std::mem; use std::process; use std::result; -use std::thread; -use std::time; use libnitrokey; use crate::error::Error; type Result<T> = result::Result<T, Error>; -type NitroFunc = dyn Fn(&mut libhid::Handle) -> Result<()>; - const PIN_TYPE: pinentry::PinType = pinentry::PinType::User; -const SEND_TRY_COUNT: i8 = 3; -const RECV_TRY_COUNT: i8 = 40; -const SEND_RECV_DELAY_MS: u64 = 200; /// Create an `error::Error` with an error message of the format `msg: err`. @@ -105,185 +95,58 @@ fn get_storage_device() -> Result<libnitrokey::Storage> { .or_else(|_| Err(Error::Error("Nitrokey device not found".to_string()))) } - -/// Send a HID feature report to the device represented by the given handle. -fn send<P>(handle: &mut libhid::Handle, report: &nitrokey::Report<P>) -> Result<()> - where P: AsRef<[u8]>, -{ - let mut retry = SEND_TRY_COUNT; - loop { - let result = handle.feature().send_to(0, report.as_ref()); - retry -= 1; - - match result { - Ok(_) => { - return Ok(()); - }, - Err(err) => { - if retry > 0 { - thread::sleep(time::Duration::from_millis(SEND_RECV_DELAY_MS)); - continue; - } else { - return Err(Error::HidError(err)); - } - }, - } - } -} - - -/// Receive a HID feature report from the device represented by the given handle. -fn receive<P>(handle: &mut libhid::Handle) -> Result<nitrokey::Report<P>> - where P: AsRef<[u8]> + Default, -{ - let mut retry = RECV_TRY_COUNT; - loop { - let mut report = nitrokey::Report::<P>::new(); - let result = handle.feature().get_from(0, report.as_mut()); - - retry -= 1; - - match result { - Ok(size) => { - if size < mem::size_of_val(&report) { - if retry > 0 { - continue; - } else { - return Err(Error::Error("Failed to receive complete report".to_string())); - } - } - - if !report.is_valid() { - if retry > 0 { - continue; - } else { - return Err(Error::Error("Failed to receive report: CRC mismatch".to_string())); - } - } - return Ok(report); - }, - - Err(err) => { - if retry > 0 { - thread::sleep(time::Duration::from_millis(SEND_RECV_DELAY_MS)); - continue; - } else { - return Err(Error::HidError(err)); - } - }, +/// Return a string representation of the given volume status. +fn get_volume_status(status: &libnitrokey::VolumeStatus) -> &'static str { + if status.active { + if status.read_only { + "read-only" + } else { + "active" } + } else { + "inactive" } } -/// Transmit a HID feature report to the nitrokey and receive a response. -fn transmit<PS, PR>(handle: &mut libhid::Handle, - report: &nitrokey::Report<PS>) - -> Result<nitrokey::Report<PR>> - where PS: AsRef<[u8]>, - PR: AsRef<[u8]> + Default, -{ - send(handle, report)?; - - // We need to give the stick some time to handle the command. If we - // don't, we might just receive stale data from before. - thread::sleep(time::Duration::from_millis(SEND_RECV_DELAY_MS)); - - receive::<PR>(handle) -} - - -/// Find and open the nitrokey device and execute a function on it. -fn nitrokey_do(function: &NitroFunc) -> Result<()> { - let hid = libhid::init()?; - // The Manager::find method is plain stupid as it still returns an - // iterable. Using it does not help in more concise error handling. - for device in hid.devices() { - if device.vendor_id() == nitrokey::VID && device.product_id() == nitrokey::PID { - return function(&mut device.open()?); - } - } - Err(Error::Error("Nitrokey device not found".to_string())) -} - - /// Pretty print the response of a status command. -fn print_status(response: &nitrokey::DeviceStatusResponse) { +fn print_status(status: &libnitrokey::StorageStatus) { println!("Status:"); // We omit displaying information about the smartcard here as this // program really is only about the SD card portion of the device. - println!(" SD card ID: {:#x}", response.active_sdcard_id); + println!(" SD card ID: {:#x}", status.serial_number_sd_card); println!(" firmware version: {}.{}", - response.version_major, - response.version_minor); + status.firmware_version_major, + status.firmware_version_minor); println!(" firmware: {}", - if response.firmware_locked != 0 { + if status.firmware_locked { "locked".to_string() } else { "unlocked".to_string() }); println!(" storage keys: {}", - if response.storage_keys_missing == 0 { + if status.stick_initialized { "created".to_string() } else { "not created".to_string() }); - println!(" user retry count: {}", - response.user_password_retry_count); - println!(" admin retry count: {}", - response.admin_password_retry_count); + println!(" user retry count: {}", status.user_retry_count); + println!(" admin retry count: {}", status.admin_retry_count); println!(" volumes:"); - println!(" unencrypted: {}", - if response.volume_active & nitrokey::VOLUME_ACTIVE_UNENCRYPTED == 0 { - "inactive" - } else if response.unencrypted_volume_read_only != 0 { - "read-only" - } else { - "active" - }); - println!(" encrypted: {}", - if response.volume_active & nitrokey::VOLUME_ACTIVE_ENCRYPTED == 0 { - "inactive" - } else if response.encrypted_volume_read_only != 0 { - "read-only" - } else { - "active" - }); - println!(" hidden: {}", - if response.volume_active & nitrokey::VOLUME_ACTIVE_HIDDEN == 0 { - "inactive" - } else if response.hidden_volume_read_only != 0 { - "read-only" - } else { - "active" - }); + println!(" unencrypted: {}", get_volume_status(&status.unencrypted_volume)); + println!(" encrypted: {}", get_volume_status(&status.encrypted_volume)); + println!(" hidden: {}", get_volume_status(&status.hidden_volume)); } /// Inquire the status of the nitrokey. fn status() -> Result<()> { - type Response = nitrokey::Response<nitrokey::DeviceStatusResponse>; - - nitrokey_do(&|handle| { - let payload = nitrokey::DeviceStatusCommand::new(); - let report = nitrokey::Report::from(payload); - - let report = transmit::<_, nitrokey::EmptyPayload>(handle, &report)?; - let response = &AsRef::<Response>::as_ref(&report.data).data; - - // TODO: We should probably check the success of the command as - // well. - if response.magic != nitrokey::MAGIC_NUMBER_STICK20_CONFIG { - let error = format!("Status response contains invalid magic: {:#x} \ - (expected: {:#x})", - response.magic, - nitrokey::MAGIC_NUMBER_STICK20_CONFIG); - return Err(Error::Error(error.to_string())); - } + let status = get_storage_device()? + .get_status() + .map_err(|err| get_error("Getting Storage status failed", &err))?; - print_status(response); - Ok(()) - }) + print_status(&status); + Ok(()) } @@ -380,53 +243,3 @@ fn run() -> i32 { fn main() { process::exit(run()); } - - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn wrong_crc() { - type Response = nitrokey::Response<nitrokey::DeviceStatusResponse>; - - nitrokey_do(&|handle| { - let payload = nitrokey::DeviceStatusCommand::new(); - let mut report = nitrokey::Report::from(payload); - - // We want to verify that we get the correct result (i.e., a - // report of the CRC mismatch) repeatedly. - for _ in 0..10 { - report.crc += 1; - - let new_report = transmit::<_, nitrokey::EmptyPayload>(handle, &report)?; - let response = AsRef::<Response>::as_ref(&new_report.data); - - assert_eq!(response.command, nitrokey::Command::GetDeviceStatus); - assert_eq!(response.command_crc, report.crc); - assert_eq!(response.command_status, nitrokey::CommandStatus::WrongCrc); - } - return Ok(()); - }) - .unwrap(); - } - - #[test] - fn device_status() { - type Response = nitrokey::Response<nitrokey::DeviceStatusResponse>; - - nitrokey_do(&|handle| { - let payload = nitrokey::DeviceStatusCommand::new(); - let report = nitrokey::Report::from(payload); - - let report = transmit::<_, nitrokey::EmptyPayload>(handle, &report)?; - let response = AsRef::<Response>::as_ref(&report.data); - - assert!(response.device_status == nitrokey::StorageStatus::Idle || - response.device_status == nitrokey::StorageStatus::Okay); - assert_eq!(response.data.magic, nitrokey::MAGIC_NUMBER_STICK20_CONFIG); - return Ok(()); - }) - .unwrap(); - } -} diff --git a/nitrocli/src/nitrokey.rs b/nitrocli/src/nitrokey.rs deleted file mode 100644 index 9f767d6..0000000 --- a/nitrocli/src/nitrokey.rs +++ /dev/null @@ -1,256 +0,0 @@ -// nitrokey.rs - -// ************************************************************************* -// * Copyright (C) 2017-2018 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/>. * -// ************************************************************************* - -use std::mem; - -use crate::crc32::crc; - - -// The Nitrokey Storage vendor ID. -pub const VID: u16 = 0x20A0; -// The Nitrokey Storage product ID. -pub const PID: u16 = 0x4109; - -// Magic number identifying a storage response. -pub const MAGIC_NUMBER_STICK20_CONFIG: u16 = 0x3318; - -// Flags indicating whether the respective volume is active or not. -pub const VOLUME_ACTIVE_UNENCRYPTED: u8 = 0b001; -pub const VOLUME_ACTIVE_ENCRYPTED: u8 = 0b010; -pub const VOLUME_ACTIVE_HIDDEN: u8 = 0b100; - - -#[derive(Debug)] -#[derive(PartialEq)] -#[repr(u8)] -pub enum Command { - // Retrieve the device status. - GetDeviceStatus = 0x2E, -} - - -/// A report is the entity we send to the Nitrokey Storage HID. -/// -/// A report is always 64 bytes in size. The last four bytes comprise a -/// CRC of the actual payload. Note that when sending or receiving a -/// report it usually is preceded by a one byte report ID. This report -/// ID is zero here and not represented in the actual report object in -/// our design. -#[repr(packed)] -pub struct Report<Payload> - where Payload: AsRef<[u8]>, -{ - // The actual payload data. A report may encapsulate a command to send - // to the stick or a response to receive from it. - pub data: Payload, - pub crc: u32, -} - - -impl<P> Report<P> - where P: AsRef<[u8]> + Default, -{ - pub fn new() -> Report<P> { - Report { - data: P::default(), - crc: 0, - } - } - - pub fn is_valid(&self) -> bool { - self.crc == crc(self.data.as_ref()) - } -} - - -impl<P> AsRef<[u8]> for Report<P> - where P: AsRef<[u8]>, -{ - fn as_ref(&self) -> &[u8] { - unsafe { mem::transmute::<&Report<P>, &[u8; 64]>(self) } - } -} - - -impl<P> From<P> for Report<P> - where P: AsRef<[u8]>, -{ - fn from(payload: P) -> Report<P> { - let crc = crc(payload.as_ref()); - Report { - data: payload, - crc: crc, - } - } -} - - -impl<P> AsMut<[u8]> for Report<P> - where P: AsRef<[u8]>, -{ - fn as_mut(&mut self) -> &mut [u8] { - unsafe { mem::transmute::<&mut Report<P>, &mut [u8; 64]>(self) } - } -} - - -pub struct EmptyPayload { - pub data: [u8; 60], -} - -impl Default for EmptyPayload { - fn default() -> EmptyPayload { - EmptyPayload { - data: [0u8; 60], - } - } -} - -impl AsRef<[u8]> for EmptyPayload { - fn as_ref(&self) -> &[u8] { - unsafe { mem::transmute::<&EmptyPayload, &[u8; 60]>(self) } - } -} - -impl<P> AsRef<Response<P>> for EmptyPayload { - fn as_ref(&self) -> &Response<P> { - unsafe { mem::transmute::<&EmptyPayload, &Response<P>>(self) } - } -} - - -macro_rules! defaultCommandType { - ( $name:ident ) => { - #[allow(dead_code)] - #[repr(packed)] - pub struct $name { - command: Command, - padding: [u8; 59], - } - } -} - -macro_rules! defaultCommandNew { - ( $name:ident, $command:ident ) => { - impl $name { - pub fn new() -> $name { - $name{ - command: Command::$command, - padding: [0; 59], - } - } - } - } -} - -macro_rules! defaultPayloadAsRef { - ( $name:ty ) => { - impl AsRef<[u8]> for $name { - fn as_ref(&self) -> &[u8] { - unsafe { mem::transmute::<&$name, &[u8; 60]>(self) } - } - } - } -} - -macro_rules! defaultCommand { - ( $name:ident, $command:ident ) => { - defaultCommandType!($name); - defaultCommandNew!($name, $command); - defaultPayloadAsRef!($name); - } -} - - -defaultCommand!(DeviceStatusCommand, GetDeviceStatus); - - -#[allow(dead_code)] -#[derive(Debug)] -#[derive(PartialEq)] -#[repr(u8)] -pub enum CommandStatus { - Okay = 0, - WrongCrc = 1, - WrongSlot = 2, - SlotNotProgrammed = 3, - WrongPassword = 4, - NotAuthorized = 5, - TimestampWarning = 6, - NoNameError = 7, -} - - -#[allow(dead_code)] -#[derive(Copy)] -#[derive(Clone)] -#[derive(Debug)] -#[derive(PartialEq)] -#[repr(u8)] -pub enum StorageStatus { - Idle = 0, - Okay = 1, - Busy = 2, - WrongPassword = 3, - BusyProgressbar = 4, - PasswordMatrixReady = 5, - NoUserPasswordUnlock = 6, - SmartcardError = 7, - SecurityBitActive = 8, -} - - -#[repr(packed)] -pub struct Response<Payload> { - pub device_status: StorageStatus, - pub command: Command, - pub command_crc: u32, - pub command_status: CommandStatus, - pub data: Payload, -} - -impl<P> AsRef<[u8]> for Response<P> { - fn as_ref(&self) -> &[u8] { - unsafe { mem::transmute::<&Response<P>, &[u8; 60]>(self) } - } -} - - -#[repr(packed)] -pub struct DeviceStatusResponse { - pub padding0: [u8; 22], - pub magic: u16, - pub unencrypted_volume_read_only: u8, - pub encrypted_volume_read_only: u8, - pub version_major: u8, - pub version_minor: u8, - pub version_build: u8, - pub version_internal: u8, - pub hidden_volume_read_only: u8, - pub firmware_locked: u8, - pub new_sdcard_found: u8, - pub sdcard_fill_with_random: u8, - pub active_sdcard_id: u32, - pub volume_active: u8, - pub new_smartcard_found: u8, - pub user_password_retry_count: u8, - pub admin_password_retry_count: u8, - pub active_smartcard_id: u32, - pub storage_keys_missing: u8, -} |