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)] |