diff options
| -rw-r--r-- | nitrocli/Makefile | 27 | ||||
| -rw-r--r-- | nitrocli/src/main.rs | 88 | ||||
| -rw-r--r-- | nitrocli/src/nitrokey.rs | 128 | 
3 files changed, 241 insertions, 2 deletions
| diff --git a/nitrocli/Makefile b/nitrocli/Makefile new file mode 100644 index 0000000..ca3a0b9 --- /dev/null +++ b/nitrocli/Makefile @@ -0,0 +1,27 @@ +# Makefile + +#/*************************************************************************** +# *   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/>. * +# ***************************************************************************/ + + +# We do not want to run the tests concurrently as the Nitrokey seems to +# have problems with that. We also do not want to introduce additional +# locking because in the normal program work flow we will not have +# multiple requests issued in parallel. +.PHONY: test +test: +	@RUST_TEST_NOCAPTURE=1 RUST_TEST_THREADS=1 cargo test diff --git a/nitrocli/src/main.rs b/nitrocli/src/main.rs index 700204d..a1f026e 100644 --- a/nitrocli/src/main.rs +++ b/nitrocli/src/main.rs @@ -34,11 +34,35 @@ mod pinentry;  use error::Error;  use std::process;  use std::result; +use std::thread; +use std::time;  type Result<T> = result::Result<T, Error>;  type NitroFunc = Fn(&mut libhid::Handle) -> Result<()>; +const SEND_RECV_DELAY_MS: u64 = 200; + + +/// 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]>, +{ +  handle.feature().send_to(0, report.as_ref())?; +  return Ok(()); +} + + +/// 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 report = nitrokey::Report::<P>::new(); +  handle.feature().get_from(0, report.as_mut())?; +  return Ok(report); +} + +  /// Find and open the nitrokey device and execute a function on it.  fn nitrokey_do(function: &NitroFunc) -> Result<()> {    let hid = libhid::init()?; @@ -60,7 +84,12 @@ fn open() -> Result<()> {      let payload = nitrokey::EnableEncryptedVolumeCommand::new(&passphrase);      let report = nitrokey::Report::from(payload); -    handle.feature().send_to(0, report.as_ref())?; +    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::<nitrokey::EmptyPayload>(handle)?;      return Ok(());    });  } @@ -72,7 +101,7 @@ fn close() -> Result<()> {      let payload = nitrokey::DisableEncryptedVolumeCommand::new();      let report = nitrokey::Report::from(payload); -    handle.feature().send_to(0, report.as_ref())?; +    send(handle, &report)?;      return Ok(());    });  } @@ -114,3 +143,58 @@ fn run() -> i32 {  fn main() {    process::exit(run());  } + + +#[cfg(test)] +mod tests { +  use super::*; + +  #[test] +  fn wrong_crc() { +    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; +        send(handle, &report).unwrap(); +        thread::sleep(time::Duration::from_millis(SEND_RECV_DELAY_MS)); + +        let new_report = receive::<nitrokey::EmptyPayload>(handle).unwrap(); +        assert!(new_report.is_valid()); + +        let response: &nitrokey::Response<nitrokey::DeviceStatusResponse> = new_report.data +                                                                                      .as_ref(); +        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() { +    nitrokey_do(&|handle| { +      let payload = nitrokey::DeviceStatusCommand::new(); +      let report = nitrokey::Report::from(payload); + +      send(handle, &report).unwrap(); +      thread::sleep(time::Duration::from_millis(SEND_RECV_DELAY_MS)); + +      let new_report = receive::<nitrokey::EmptyPayload>(handle).unwrap(); +      assert!(new_report.is_valid()); + +      let response: &nitrokey::Response<nitrokey::DeviceStatusResponse> = new_report.data.as_ref(); + +      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 index 37ba597..87283c0 100644 --- a/nitrocli/src/nitrokey.rs +++ b/nitrocli/src/nitrokey.rs @@ -27,6 +27,9 @@ 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; +  #[derive(Debug)]  #[derive(PartialEq)] @@ -36,6 +39,8 @@ pub enum Command {    EnableEncryptedVolume = 0x20,    // The command to disable the encrypted volume.    DisableEncryptedVolume = 0x21, +  // Retrieve the device status. +  GetDeviceStatus = 0x2E,  } @@ -57,6 +62,22 @@ pub struct Report<Payload>  } +impl<P> Report<P> +  where P: AsRef<[u8]> + Default, +{ +  pub fn new() -> Report<P> { +    return Report { +      data: P::default(), +      crc: 0, +    }; +  } + +  pub fn is_valid(&self) -> bool { +    return self.crc == crc(self.data.as_ref()); +  } +} + +  impl<P> AsRef<[u8]> for Report<P>    where P: AsRef<[u8]>,  { @@ -79,6 +100,40 @@ impl<P> From<P> for Report<P>  } +impl<P> AsMut<[u8]> for Report<P> +  where P: AsRef<[u8]>, +{ +  fn as_mut(&mut self) -> &mut [u8] { +    unsafe { return mem::transmute::<&mut Report<P>, &mut [u8; 64]>(self) }; +  } +} + + +pub struct EmptyPayload { +  pub data: [u8; 60], +} + +impl Default for EmptyPayload { +  fn default() -> EmptyPayload { +    return EmptyPayload { +      data: [0u8; 60], +    }; +  } +} + +impl AsRef<[u8]> for EmptyPayload { +  fn as_ref(&self) -> &[u8] { +    unsafe { return mem::transmute::<&EmptyPayload, &[u8; 60]>(self) }; +  } +} + +impl<P> AsRef<Response<P>> for EmptyPayload { +  fn as_ref(&self) -> &Response<P> { +    unsafe { return mem::transmute::<&EmptyPayload, &Response<P>>(self) }; +  } +} + +  macro_rules! defaultCommandType {    ( $name:ident ) => {      #[allow(dead_code)] @@ -157,6 +212,79 @@ impl EnableEncryptedVolumeCommand {  defaultPayloadAsRef!(EnableEncryptedVolumeCommand);  defaultCommand!(DisableEncryptedVolumeCommand, DisableEncryptedVolume); +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(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 { return 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 padding1: u8, +  pub version_major: u8, +  pub padding2: u8, +  pub version_minor: 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, +}  #[cfg(test)] | 
