diff options
-rw-r--r-- | nitrocli/CHANGELOG.md | 2 | ||||
-rw-r--r-- | nitrocli/Cargo.lock | 4 | ||||
-rw-r--r-- | nitrocli/Cargo.toml | 2 | ||||
-rw-r--r-- | nitrocli/src/commands.rs | 6 | ||||
-rw-r--r-- | nitrocli/src/pinentry.rs | 4 | ||||
-rw-r--r-- | nitrokey/CHANGELOG.md | 15 | ||||
-rw-r--r-- | nitrokey/Cargo.toml | 2 | ||||
-rw-r--r-- | nitrokey/TODO.md | 2 | ||||
-rw-r--r-- | nitrokey/examples/list-devices.rs | 2 | ||||
-rw-r--r-- | nitrokey/src/auth.rs | 37 | ||||
-rw-r--r-- | nitrokey/src/config.rs | 14 | ||||
-rw-r--r-- | nitrokey/src/device/mod.rs | 284 | ||||
-rw-r--r-- | nitrokey/src/device/storage.rs | 14 | ||||
-rw-r--r-- | nitrokey/src/error.rs | 6 | ||||
-rw-r--r-- | nitrokey/src/lib.rs | 4 | ||||
-rw-r--r-- | nitrokey/src/util.rs | 61 | ||||
-rw-r--r-- | nitrokey/tests/device.rs | 15 | ||||
-rw-r--r-- | nitrokey/tests/pws.rs | 2 |
18 files changed, 309 insertions, 167 deletions
diff --git a/nitrocli/CHANGELOG.md b/nitrocli/CHANGELOG.md index 02812e0..0204e6d 100644 --- a/nitrocli/CHANGELOG.md +++ b/nitrocli/CHANGELOG.md @@ -6,7 +6,7 @@ Unreleased - Replaced `argparse` with `structopt` - Removed the `argparse` dependency - Made the --verbose and --model options global -- Bumped `nitrokey` dependency to `0.5.1` +- Bumped `nitrokey` dependency to `0.6.0` 0.3.1 diff --git a/nitrocli/Cargo.lock b/nitrocli/Cargo.lock index 5f9eacf..550da4a 100644 --- a/nitrocli/Cargo.lock +++ b/nitrocli/Cargo.lock @@ -68,7 +68,7 @@ version = "0.3.1" dependencies = [ "base32 0.4.0", "libc 0.2.66", - "nitrokey 0.5.1", + "nitrokey 0.6.0", "nitrokey-test 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", "nitrokey-test-state 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "regex 1.3.1 (registry+https://github.com/rust-lang/crates.io-index)", @@ -77,7 +77,7 @@ dependencies = [ [[package]] name = "nitrokey" -version = "0.5.1" +version = "0.6.0" dependencies = [ "lazy_static 1.4.0", "libc 0.2.66", diff --git a/nitrocli/Cargo.toml b/nitrocli/Cargo.toml index b69a2bb..d1d7ae2 100644 --- a/nitrocli/Cargo.toml +++ b/nitrocli/Cargo.toml @@ -49,7 +49,7 @@ version = "0.4.0" version = "0.2" [dependencies.nitrokey] -version = "0.5.1" +version = "0.6" [dependencies.structopt] version = "0.3.7" diff --git a/nitrocli/src/commands.rs b/nitrocli/src/commands.rs index e361509..08dad04 100644 --- a/nitrocli/src/commands.rs +++ b/nitrocli/src/commands.rs @@ -343,7 +343,7 @@ fn print_status( ctx, r#"Status: model: {model} - serial number: 0x{id} + serial number: {id} firmware version: {fwv} user retry count: {urc} admin retry count: {arc}"#, @@ -393,7 +393,7 @@ pub fn list(ctx: &mut args::ExecCtx<'_>, no_connect: bool) -> Result<()> { .map(|m| m.to_string()) .unwrap_or_else(|| "unknown".into()); let serial_number = match device_info.serial_number { - Some(serial_number) => format!("0x{}", serial_number), + Some(serial_number) => serial_number.to_string(), None => { // Storage devices do not have the serial number present in // the device information. We have to connect to them to @@ -402,7 +402,7 @@ pub fn list(ctx: &mut args::ExecCtx<'_>, no_connect: bool) -> Result<()> { "N/A".to_string() } else { let device = manager.connect_path(device_info.path.clone())?; - format!("0x{}", device.get_serial_number()?) + device.get_serial_number()?.to_string() } } }; diff --git a/nitrocli/src/pinentry.rs b/nitrocli/src/pinentry.rs index fd47657..af2b4dc 100644 --- a/nitrocli/src/pinentry.rs +++ b/nitrocli/src/pinentry.rs @@ -54,7 +54,7 @@ pub trait SecretEntry: fmt::Debug { pub struct PinEntry { pin_type: PinType, model: nitrokey::Model, - serial: String, + serial: nitrokey::SerialNumber, } impl PinEntry { @@ -127,7 +127,7 @@ impl SecretEntry for PinEntry { #[derive(Debug)] pub struct PwdEntry { model: nitrokey::Model, - serial: String, + serial: nitrokey::SerialNumber, } impl PwdEntry { diff --git a/nitrokey/CHANGELOG.md b/nitrokey/CHANGELOG.md index cba0e83..e2dd8a7 100644 --- a/nitrokey/CHANGELOG.md +++ b/nitrokey/CHANGELOG.md @@ -3,6 +3,21 @@ Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org> SPDX-License-Identifier: CC0-1.0 --> +# v0.6.0 (2020-02-03) +- Add `String` value to the `Error::UnexpectedError` variant. +- Always store serial numbers as structs: + - Introduce the `SerialNumber` struct. + - Change the return type of `Device::get_serial_number` from `Result<String, + _>` to `Result<SerialNumber, _>`. + - Change the type of the field `DeviceInfo.serial_number` from + `Option<String>` to `Option<SerialNumber>`. +- Use `NK_get_status` instead of `NK_read_config` to implement the + `Device::get_config` function. + +# v0.5.2 (2020-01-28) +- Use `CString` to store the temporary password instead of `Vec<u8>`. +- Regenerate temporary passwords if they would contain a null byte. + # v0.5.1 (2020-01-15) - Fix serial number formatting for Nitrokey Pro devices with firmware 0.8 or older in the `list_devices` function. diff --git a/nitrokey/Cargo.toml b/nitrokey/Cargo.toml index 59af067..f2d859d 100644 --- a/nitrokey/Cargo.toml +++ b/nitrokey/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nitrokey" -version = "0.5.1" +version = "0.6.0" authors = ["Robin Krahl <robin.krahl@ireas.org>"] edition = "2018" homepage = "https://code.ireas.org/nitrokey-rs/" diff --git a/nitrokey/TODO.md b/nitrokey/TODO.md index 92d4b04..e50d354 100644 --- a/nitrokey/TODO.md +++ b/nitrokey/TODO.md @@ -6,5 +6,7 @@ SPDX-License-Identifier: CC0-1.0 - Clear passwords from memory. - Lock password safe in `PasswordSafe::drop()` (see [nitrokey-storage-firmware issue 65][]). +- Consider only regenerating the null bytes instead of the complete password in + `util::generate_password`. [nitrokey-storage-firmware issue 65]: https://github.com/Nitrokey/nitrokey-storage-firmware/issues/65 diff --git a/nitrokey/examples/list-devices.rs b/nitrokey/examples/list-devices.rs index 47fa054..0066f8c 100644 --- a/nitrokey/examples/list-devices.rs +++ b/nitrokey/examples/list-devices.rs @@ -17,7 +17,7 @@ fn main() -> Result<(), nitrokey::Error> { let model = device.get_model(); let status = device.get_status()?; println!( - "{}\t{}\t{}\t\t\t{:08x}", + "{}\t{}\t{}\t\t\t{}", device_info.path, model, status.firmware_version, status.serial_number ); } diff --git a/nitrokey/src/auth.rs b/nitrokey/src/auth.rs index cab1021..6748ca1 100644 --- a/nitrokey/src/auth.rs +++ b/nitrokey/src/auth.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT use std::convert::TryFrom as _; +use std::ffi::CString; use std::marker; use std::ops; use std::os::raw::c_char; @@ -117,9 +118,7 @@ pub trait Authenticate<'a> { } trait AuthenticatedDevice<T> { - fn new(device: T, temp_password: Vec<u8>) -> Self; - - fn temp_password_ptr(&self) -> *const c_char; + fn new(device: T, temp_password: CString) -> Self; } /// A Nitrokey device with user authentication. @@ -134,7 +133,7 @@ trait AuthenticatedDevice<T> { #[derive(Debug)] pub struct User<'a, T: Device<'a>> { device: T, - temp_password: Vec<u8>, + temp_password: CString, marker: marker::PhantomData<&'a T>, } @@ -150,7 +149,7 @@ pub struct User<'a, T: Device<'a>> { #[derive(Debug)] pub struct Admin<'a, T: Device<'a>> { device: T, - temp_password: Vec<u8>, + temp_password: CString, marker: marker::PhantomData<&'a T>, } @@ -169,7 +168,7 @@ where Err(err) => return Err((device, err)), }; let password_ptr = password.as_ptr(); - let temp_password_ptr = temp_password.as_ptr() as *const c_char; + let temp_password_ptr = temp_password.as_ptr(); match callback(password_ptr, temp_password_ptr) { 0 => Ok(A::new(device, temp_password)), rv => Err((device, Error::from(rv))), @@ -234,29 +233,25 @@ impl<'a, T: Device<'a>> ops::DerefMut for User<'a, T> { impl<'a, T: Device<'a>> GenerateOtp for User<'a, T> { fn get_hotp_code(&mut self, slot: u8) -> Result<String, Error> { result_from_string(unsafe { - nitrokey_sys::NK_get_hotp_code_PIN(slot, self.temp_password_ptr()) + nitrokey_sys::NK_get_hotp_code_PIN(slot, self.temp_password.as_ptr()) }) } fn get_totp_code(&self, slot: u8) -> Result<String, Error> { result_from_string(unsafe { - nitrokey_sys::NK_get_totp_code_PIN(slot, 0, 0, 0, self.temp_password_ptr()) + nitrokey_sys::NK_get_totp_code_PIN(slot, 0, 0, 0, self.temp_password.as_ptr()) }) } } impl<'a, T: Device<'a>> AuthenticatedDevice<T> for User<'a, T> { - fn new(device: T, temp_password: Vec<u8>) -> Self { + fn new(device: T, temp_password: CString) -> Self { User { device, temp_password, marker: marker::PhantomData, } } - - fn temp_password_ptr(&self) -> *const c_char { - self.temp_password.as_ptr() as *const c_char - } } impl<'a, T: Device<'a>> ops::Deref for Admin<'a, T> { @@ -318,7 +313,7 @@ impl<'a, T: Device<'a>> Admin<'a, T> { raw_config.scrollock, raw_config.user_password, false, - self.temp_password_ptr(), + self.temp_password.as_ptr(), ) }) } @@ -337,7 +332,7 @@ impl<'a, T: Device<'a>> ConfigureOtp for Admin<'a, T> { raw_data.use_enter, raw_data.use_token_id, raw_data.token_id.as_ptr(), - self.temp_password_ptr(), + self.temp_password.as_ptr(), ) }) } @@ -354,36 +349,32 @@ impl<'a, T: Device<'a>> ConfigureOtp for Admin<'a, T> { raw_data.use_enter, raw_data.use_token_id, raw_data.token_id.as_ptr(), - self.temp_password_ptr(), + self.temp_password.as_ptr(), ) }) } fn erase_hotp_slot(&mut self, slot: u8) -> Result<(), Error> { get_command_result(unsafe { - nitrokey_sys::NK_erase_hotp_slot(slot, self.temp_password_ptr()) + nitrokey_sys::NK_erase_hotp_slot(slot, self.temp_password.as_ptr()) }) } fn erase_totp_slot(&mut self, slot: u8) -> Result<(), Error> { get_command_result(unsafe { - nitrokey_sys::NK_erase_totp_slot(slot, self.temp_password_ptr()) + nitrokey_sys::NK_erase_totp_slot(slot, self.temp_password.as_ptr()) }) } } impl<'a, T: Device<'a>> AuthenticatedDevice<T> for Admin<'a, T> { - fn new(device: T, temp_password: Vec<u8>) -> Self { + fn new(device: T, temp_password: CString) -> Self { Admin { device, temp_password, marker: marker::PhantomData, } } - - fn temp_password_ptr(&self) -> *const c_char { - self.temp_password.as_ptr() as *const c_char - } } impl<'a> Authenticate<'a> for DeviceWrapper<'a> { diff --git a/nitrokey/src/config.rs b/nitrokey/src/config.rs index cb678d7..120a51b 100644 --- a/nitrokey/src/config.rs +++ b/nitrokey/src/config.rs @@ -83,13 +83,13 @@ impl convert::TryFrom<Config> for RawConfig { } } -impl From<[u8; 5]> for RawConfig { - fn from(data: [u8; 5]) -> Self { - RawConfig { - numlock: data[0], - capslock: data[1], - scrollock: data[2], - user_password: data[3] != 0, +impl From<&nitrokey_sys::NK_status> for RawConfig { + fn from(status: &nitrokey_sys::NK_status) -> Self { + Self { + numlock: status.config_numlock, + capslock: status.config_capslock, + scrollock: status.config_scrolllock, + user_password: status.otp_user_password, } } } diff --git a/nitrokey/src/device/mod.rs b/nitrokey/src/device/mod.rs index 0234bf0..067fdf6 100644 --- a/nitrokey/src/device/mod.rs +++ b/nitrokey/src/device/mod.rs @@ -8,18 +8,17 @@ mod wrapper; use std::convert::{TryFrom, TryInto}; use std::ffi; use std::fmt; +use std::str; -use libc; use nitrokey_sys; use crate::auth::Authenticate; use crate::config::{Config, RawConfig}; -use crate::error::{CommunicationError, Error}; +use crate::error::{CommunicationError, Error, LibraryError}; use crate::otp::GenerateOtp; use crate::pws::GetPasswordSafe; use crate::util::{ - get_command_result, get_cstring, get_last_error, owned_str_from_ptr, result_from_string, - result_or_error, + get_command_result, get_cstring, owned_str_from_ptr, result_or_error, run_with_string, }; pub use pro::Pro; @@ -71,6 +70,98 @@ impl TryFrom<nitrokey_sys::NK_device_model> for Model { } } +/// Serial number of a Nitrokey device. +/// +/// The serial number can be formatted as a string using the [`ToString`][] trait, and it can be +/// parsed from a string using the [`FromStr`][] trait. It can also be represented as a 32-bit +/// unsigned integer using [`as_u32`][]. This integer is the ID of the smartcard of the Nitrokey +/// device. +/// +/// Neither the format of the string representation nor the integer representation are guaranteed +/// to stay the same for new firmware versions. +/// +/// [`as_u32`]: #method.as_u32 +/// [`FromStr`]: #impl-FromStr +/// [`ToString`]: #impl-ToString +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SerialNumber { + value: u32, +} + +impl SerialNumber { + /// Creates an emtpty serial number. + /// + /// This function can be used to create a placeholder value or to compare a `SerialNumber` + /// instance with an empty serial number. + pub fn empty() -> Self { + SerialNumber::new(0) + } + + fn new(value: u32) -> Self { + SerialNumber { value } + } + + /// Returns the integer reprensentation of this serial number. + /// + /// This integer currently is the ID of the smartcard of the Nitrokey device. Upcoming + /// firmware versions might change the meaning of this representation, or add additional + /// components to the serial number. + // To provide a stable API even if the internal representation of SerialNumber changes, we want + // to borrow SerialNumber instead of copying it even if it might be less efficient. + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn as_u32(&self) -> u32 { + self.value + } +} + +impl fmt::Display for SerialNumber { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:#010x}", self.value) + } +} + +impl str::FromStr for SerialNumber { + type Err = Error; + + /// Try to parse a serial number from a hex string. + /// + /// The input string must be a valid hex string. Optionally, it can include a `0x` prefix. + /// + /// # Errors + /// + /// - [`InvalidHexString`][] if the given string is not a valid hex string + /// + /// # Example + /// + /// ```no_run + /// use std::convert::TryFrom; + /// use nitrokey::{DeviceInfo, Error, SerialNumber}; + /// + /// fn find_device(serial_number: &str) -> Result<Option<DeviceInfo>, Error> { + /// let serial_number: SerialNumber = serial_number.parse()?; + /// Ok(nitrokey::list_devices()? + /// .into_iter() + /// .filter(|device| device.serial_number == Some(serial_number)) + /// .next()) + /// } + /// + /// ``` + /// + /// [`InvalidHexString`]: enum.LibraryError.html#variant.InvalidHexString + fn from_str(s: &str) -> Result<SerialNumber, Error> { + // ignore leading 0x + let hex_string = if s.starts_with("0x") { + s.split_at(2).1 + } else { + s + }; + + u32::from_str_radix(hex_string, 16) + .map(SerialNumber::new) + .map_err(|_| LibraryError::InvalidHexString.into()) + } +} + /// Connection information for a Nitrokey device. #[derive(Clone, Debug, PartialEq)] pub struct DeviceInfo { @@ -78,9 +169,8 @@ pub struct DeviceInfo { pub model: Option<Model>, /// The USB device path. pub path: String, - /// The serial number as a 8-character hex string, or `None` if the device does not expose its - /// serial number. - pub serial_number: Option<String>, + /// The serial number of the device, or `None` if the device does not expose its serial number. + pub serial_number: Option<SerialNumber>, } impl TryFrom<&nitrokey_sys::NK_device_info> for DeviceInfo { @@ -110,45 +200,42 @@ impl fmt::Display for DeviceInfo { None => write!(f, "Unsupported Nitrokey model")?, } write!(f, " at {} with ", self.path)?; - match &self.serial_number { - Some(ref serial_number) => write!(f, "serial no. {}", serial_number), + match self.serial_number { + Some(serial_number) => write!(f, "serial no. {}", serial_number), None => write!(f, "an unknown serial number"), } } } -/// Parses a serial number returned by hidapi and transforms it to the Nitrokey format. +/// Parses a serial number returned by hidapi. /// /// If the serial number is all zero, this function returns `None`. Otherwise, it uses the last -/// eight characters. If these are all zero, the first eight characters are used instead. This -/// function also makes sure that the returned string is lowercase, consistent with libnitrokey’s -/// hex string formatting. +/// eight characters. If these are all zero, the first eight characters are used instead. The +/// selected substring is parse as a hex string and its integer value is returned from the +/// function. If the string cannot be parsed, this function returns `None`. /// /// The reason for this behavior is that the Nitrokey Storage does not report its serial number at /// all (all zero value), while the Nitrokey Pro with firmware 0.9 or later writes its serial /// number to the last eight characters. Nitrokey Pro devices with firmware 0.8 or earlier wrote /// their serial number to the first eight characters. -fn get_hidapi_serial_number(serial_number: &str) -> Option<String> { +fn get_hidapi_serial_number(serial_number: &str) -> Option<SerialNumber> { let len = serial_number.len(); if len < 8 { - // The serial number in the USB descriptor has 12 bytes, we need at least four of them + // The serial number in the USB descriptor has 12 bytes, we need at least four return None; } let iter = serial_number.char_indices().rev(); let first_non_null = iter.skip_while(|(_, c)| *c == '0').next(); if let Some((i, _)) = first_non_null { - if len - i < 8 { + let substr = if len - i < 8 { // The last eight characters contain at least one non-zero character --> use them - let mut serial_number = serial_number.split_at(len - 8).1.to_string(); - serial_number.make_ascii_lowercase(); - Some(serial_number) + serial_number.split_at(len - 8).1 } else { // The last eight characters are all zero --> use the first eight - let mut serial_number = serial_number.split_at(8).0.to_string(); - serial_number.make_ascii_lowercase(); - Some(serial_number) - } + serial_number.split_at(8).0 + }; + substr.parse().ok() } else { // The serial number is all zero None @@ -176,7 +263,7 @@ pub struct Status { /// The firmware version of the device. pub firmware_version: FirmwareVersion, /// The serial number of the device. - pub serial_number: u32, + pub serial_number: SerialNumber, /// The configuration of the device. pub config: Config, } @@ -188,14 +275,8 @@ impl From<nitrokey_sys::NK_status> for Status { major: status.firmware_version_major, minor: status.firmware_version_minor, }, - serial_number: status.serial_number_smart_card, - config: RawConfig { - numlock: status.config_numlock, - capslock: status.config_capslock, - scrollock: status.config_scrolllock, - user_password: status.otp_user_password, - } - .into(), + serial_number: SerialNumber::new(status.serial_number_smart_card), + config: RawConfig::from(&status).into(), } } } @@ -263,7 +344,7 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt /// let device = manager.connect()?; /// let status = device.get_status()?; /// println!("Firmware version: {}", status.firmware_version); - /// println!("Serial number: {:x}", status.serial_number); + /// println!("Serial number: {}", status.serial_number); /// # Ok::<(), nitrokey::Error>(()) /// ``` /// @@ -273,8 +354,9 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt /// [`StorageStatus`]: struct.StorageStatus.html fn get_status(&self) -> Result<Status, Error>; - /// Returns the serial number of the Nitrokey device. The serial number is the string - /// representation of a hex number. + /// Returns the serial number of the Nitrokey device. + /// + /// For display purpuses, the serial number should be formatted as an 8-digit hex string. /// /// # Example /// @@ -292,8 +374,10 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt /// # Ok(()) /// # } /// ``` - fn get_serial_number(&self) -> Result<String, Error> { - result_from_string(unsafe { nitrokey_sys::NK_device_serial_number() }) + fn get_serial_number(&self) -> Result<SerialNumber, Error> { + run_with_string(unsafe { nitrokey_sys::NK_device_serial_number() }, |s| { + s.parse() + }) } /// Returns the number of remaining authentication attempts for the user. The total number of @@ -388,14 +472,17 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt /// # } /// ``` fn get_config(&self) -> Result<Config, Error> { - let config_ptr = unsafe { nitrokey_sys::NK_read_config() }; - if config_ptr.is_null() { - return Err(get_last_error()); - } - let config_array_ptr = config_ptr as *const [u8; 5]; - let raw_config = unsafe { RawConfig::from(*config_array_ptr) }; - unsafe { libc::free(config_ptr as *mut libc::c_void) }; - Ok(raw_config.into()) + let mut raw_status = nitrokey_sys::NK_status { + firmware_version_major: 0, + firmware_version_minor: 0, + serial_number_smart_card: 0, + config_numlock: 0, + config_capslock: 0, + config_scrolllock: 0, + otp_user_password: false, + }; + get_command_result(unsafe { nitrokey_sys::NK_get_status(&mut raw_status) })?; + Ok(RawConfig::from(&raw_status).into()) } /// Changes the administrator PIN. @@ -625,44 +712,77 @@ pub(crate) fn connect_enum(model: Model) -> bool { #[cfg(test)] mod tests { - use super::get_hidapi_serial_number; + use std::str::FromStr; + + use super::{get_hidapi_serial_number, LibraryError, SerialNumber}; #[test] - fn hidapi_serial_number() { - assert_eq!(None, get_hidapi_serial_number("")); - assert_eq!(None, get_hidapi_serial_number("00000000000000000")); - assert_eq!(None, get_hidapi_serial_number("1234")); - assert_eq!( - Some("00001234".to_string()), - get_hidapi_serial_number("00001234") - ); - assert_eq!( - Some("00001234".to_string()), - get_hidapi_serial_number("000000001234") - ); - assert_eq!( - Some("00001234".to_string()), - get_hidapi_serial_number("100000001234") - ); - assert_eq!( - Some("12340000".to_string()), - get_hidapi_serial_number("123400000000") - ); - assert_eq!( - Some("00005678".to_string()), - get_hidapi_serial_number("000000000000000000005678") - ); - assert_eq!( - Some("00001234".to_string()), - get_hidapi_serial_number("000012340000000000000000") - ); - assert_eq!( - Some("0000ffff".to_string()), - get_hidapi_serial_number("00000000000000000000FFFF") - ); - assert_eq!( - Some("0000ffff".to_string()), - get_hidapi_serial_number("00000000000000000000ffff") - ); + fn test_serial_number_display() { + fn assert_str(s: &str, n: u32) { + assert_eq!(s.to_owned(), SerialNumber::new(n).to_string()); + } + + assert_str("0x00000000", 0); + assert_str("0x00001000", 0x1000); + assert_str("0x12345678", 0x12345678); + } + + #[test] + fn test_serial_number_try_from() { + fn assert_ok(v: u32, s: &str) { + assert_eq!(SerialNumber::new(v), SerialNumber::from_str(s).unwrap()); + assert_eq!( + SerialNumber::new(v), + SerialNumber::from_str(format!("0x{}", s).as_ref()).unwrap() + ); + } + + fn assert_err(s: &str) { + match SerialNumber::from_str(s).unwrap_err() { + super::Error::LibraryError(LibraryError::InvalidHexString) => {} + err => assert!( + false, + "expected InvalidHexString error, got {} (input {})", + err, s + ), + } + } + + assert_ok(0x1234, "1234"); + assert_ok(0x1234, "01234"); + assert_ok(0x1234, "001234"); + assert_ok(0x1234, "0001234"); + + assert_ok(0, "0"); + assert_ok(0xdeadbeef, "deadbeef"); + + assert_err("deadpork"); + assert_err("blubb"); + assert_err(""); + } + + #[test] + fn test_get_hidapi_serial_number() { + fn assert_none(s: &str) { + assert_eq!(None, get_hidapi_serial_number(s)); + } + + fn assert_some(n: u32, s: &str) { + assert_eq!(Some(SerialNumber::new(n)), get_hidapi_serial_number(s)); + } + + assert_none(""); + assert_none("00000000000000000"); + assert_none("blubb"); + assert_none("1234"); + + assert_some(0x1234, "00001234"); + assert_some(0x1234, "000000001234"); + assert_some(0x1234, "100000001234"); + assert_some(0x12340000, "123400000000"); + assert_some(0x5678, "000000000000000000005678"); + assert_some(0x1234, "000012340000000000000000"); + assert_some(0xffff, "00000000000000000000FFFF"); + assert_some(0xffff, "00000000000000000000ffff"); } } diff --git a/nitrokey/src/device/storage.rs b/nitrokey/src/device/storage.rs index deb2844..5669a91 100644 --- a/nitrokey/src/device/storage.rs +++ b/nitrokey/src/device/storage.rs @@ -7,7 +7,7 @@ use std::ops; use nitrokey_sys; -use crate::device::{Device, FirmwareVersion, Model, Status}; +use crate::device::{Device, FirmwareVersion, Model, SerialNumber, Status}; use crate::error::{CommandError, Error}; use crate::otp::GenerateOtp; use crate::util::{get_command_result, get_cstring, get_last_error}; @@ -678,7 +678,7 @@ impl<'a> Storage<'a> { if usage_data.write_level_min > usage_data.write_level_max || usage_data.write_level_max > 100 { - Err(Error::UnexpectedError) + Err(Error::UnexpectedError("Invalid write levels".to_owned())) } else { Ok(ops::Range { start: usage_data.write_level_min, @@ -708,10 +708,14 @@ impl<'a> Storage<'a> { match status { 0..=100 => u8::try_from(status) .map(OperationStatus::Ongoing) - .map_err(|_| Error::UnexpectedError), + .map_err(|_| { + Error::UnexpectedError("Cannot create u8 from operation status".to_owned()) + }), -1 => Ok(OperationStatus::Idle), -2 => Err(get_last_error()), - _ => Err(Error::UnexpectedError), + _ => Err(Error::UnexpectedError( + "Invalid operation status".to_owned(), + )), } } @@ -823,7 +827,7 @@ impl<'a> Device<'a> for Storage<'a> { let storage_status = self.get_storage_status()?; status.firmware_version = storage_status.firmware_version; - status.serial_number = storage_status.serial_number_smart_card; + status.serial_number = SerialNumber::new(storage_status.serial_number_smart_card); Ok(status) } diff --git a/nitrokey/src/error.rs b/nitrokey/src/error.rs index f9af594..7bea3f2 100644 --- a/nitrokey/src/error.rs +++ b/nitrokey/src/error.rs @@ -25,7 +25,7 @@ pub enum Error { /// An error that occurred during random number generation. RandError(Box<dyn error::Error>), /// An error that is caused by an unexpected value returned by libnitrokey. - UnexpectedError, + UnexpectedError(String), /// An unknown error returned by libnitrokey. UnknownError(i64), /// An error caused by a Nitrokey model that is not supported by this crate. @@ -102,7 +102,7 @@ impl error::Error for Error { Error::LibraryError(ref err) => Some(err), Error::PoisonError(ref err) => Some(err), Error::RandError(ref err) => Some(err.as_ref()), - Error::UnexpectedError => None, + Error::UnexpectedError(_) => None, Error::UnknownError(_) => None, Error::UnsupportedModelError => None, Error::Utf8Error(ref err) => Some(err), @@ -119,7 +119,7 @@ impl fmt::Display for Error { Error::LibraryError(ref err) => write!(f, "Library error: {}", err), Error::PoisonError(_) => write!(f, "Internal error: poisoned lock"), Error::RandError(ref err) => write!(f, "RNG error: {}", err), - Error::UnexpectedError => write!(f, "An unexpected error occurred"), + Error::UnexpectedError(ref s) => write!(f, "An unexpected error occurred: {}", s), Error::UnknownError(ref err) => write!(f, "Unknown error: {}", err), Error::UnsupportedModelError => write!(f, "Unsupported Nitrokey model"), Error::Utf8Error(ref err) => write!(f, "UTF-8 error: {}", err), diff --git a/nitrokey/src/lib.rs b/nitrokey/src/lib.rs index 9efad91..92247d7 100644 --- a/nitrokey/src/lib.rs +++ b/nitrokey/src/lib.rs @@ -138,8 +138,8 @@ use nitrokey_sys; pub use crate::auth::{Admin, Authenticate, User}; pub use crate::config::Config; pub use crate::device::{ - Device, DeviceInfo, DeviceWrapper, Model, OperationStatus, Pro, SdCardData, Status, Storage, - StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, + Device, DeviceInfo, DeviceWrapper, Model, OperationStatus, Pro, SdCardData, SerialNumber, + Status, Storage, StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, }; pub use crate::error::{CommandError, CommunicationError, Error, LibraryError}; pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; diff --git a/nitrokey/src/util.rs b/nitrokey/src/util.rs index 5a56c55..b17b071 100644 --- a/nitrokey/src/util.rs +++ b/nitrokey/src/util.rs @@ -30,26 +30,38 @@ pub enum LogLevel { DebugL2, } +pub fn str_from_ptr<'a>(ptr: *const c_char) -> Result<&'a str, Error> { + unsafe { CStr::from_ptr(ptr) }.to_str().map_err(Error::from) +} + pub fn owned_str_from_ptr(ptr: *const c_char) -> Result<String, Error> { - unsafe { CStr::from_ptr(ptr) } - .to_str() - .map(String::from) - .map_err(Error::from) + str_from_ptr(ptr).map(ToOwned::to_owned) } -pub fn result_from_string(ptr: *const c_char) -> Result<String, Error> { +pub fn run_with_string<R, F>(ptr: *const c_char, op: F) -> Result<R, Error> +where + F: FnOnce(&str) -> Result<R, Error>, +{ if ptr.is_null() { - return Err(Error::UnexpectedError); + return Err(Error::UnexpectedError( + "libnitrokey returned a null pointer".to_owned(), + )); } - let s = owned_str_from_ptr(ptr)?; + let result = str_from_ptr(ptr).and_then(op); unsafe { free(ptr as *mut c_void) }; - // An empty string can both indicate an error or be a valid return value. In this case, we - // have to check the last command status to decide what to return. - if s.is_empty() { - get_last_result().map(|_| s) - } else { - Ok(s) - } + result +} + +pub fn result_from_string(ptr: *const c_char) -> Result<String, Error> { + run_with_string(ptr, |s| { + // An empty string can both indicate an error or be a valid return value. In this case, we + // have to check the last command status to decide what to return. + if s.is_empty() { + get_last_result().map(|_| s.to_owned()) + } else { + Ok(s.to_owned()) + } + }) } pub fn result_or_error<T>(value: T) -> Result<T, Error> { @@ -69,16 +81,21 @@ pub fn get_last_result() -> Result<(), Error> { } pub fn get_last_error() -> Error { - match get_last_result() { - Ok(()) => Error::UnexpectedError, - Err(err) => err, - } + get_last_result().err().unwrap_or_else(|| { + Error::UnexpectedError("Expected an error, but command status is zero".to_owned()) + }) } -pub fn generate_password(length: usize) -> Result<Vec<u8>, Error> { - let mut data = vec![0u8; length]; - OsRng.fill_bytes(&mut data[..]); - Ok(data) +pub fn generate_password(length: usize) -> Result<CString, Error> { + loop { + // Randomly generate a password until we get a string *without* null bytes. Otherwise + // the string would be cut off prematurely due to null-termination in C. + let mut data = vec![0u8; length]; + OsRng.fill_bytes(&mut data[..]); + if let Ok(s) = CString::new(data) { + return Ok(s); + } + } } pub fn get_cstring<T: Into<Vec<u8>>>(s: T) -> Result<CString, Error> { diff --git a/nitrokey/tests/device.rs b/nitrokey/tests/device.rs index a88c956..1669d9d 100644 --- a/nitrokey/tests/device.rs +++ b/nitrokey/tests/device.rs @@ -10,7 +10,7 @@ use std::{thread, time}; use nitrokey::{ Authenticate, CommandError, CommunicationError, Config, ConfigureOtp, Device, DeviceInfo, Error, GenerateOtp, GetPasswordSafe, LibraryError, OperationStatus, OtpMode, OtpSlotData, - Storage, VolumeMode, DEFAULT_ADMIN_PIN, DEFAULT_USER_PIN, + SerialNumber, Storage, VolumeMode, DEFAULT_ADMIN_PIN, DEFAULT_USER_PIN, }; use nitrokey_test::test as test_device; @@ -47,8 +47,7 @@ fn list_devices(_device: DeviceWrapper) { nitrokey::Model::Pro => { assert!(device.serial_number.is_some()); let serial_number = device.serial_number.unwrap(); - assert!(!serial_number.is_empty()); - assert_valid_serial_number(&serial_number); + assert!(serial_number != SerialNumber::empty()); } nitrokey::Model::Storage => { assert_eq!(None, device.serial_number); @@ -157,20 +156,14 @@ fn disconnect(device: DeviceWrapper) { fn get_status(device: DeviceWrapper) { let status = unwrap_ok!(device.get_status()); assert_ok!(status.firmware_version, device.get_firmware_version()); - let serial_number = format!("{:08x}", status.serial_number); - assert_ok!(serial_number, device.get_serial_number()); + assert_ok!(status.serial_number, device.get_serial_number()); assert_ok!(status.config, device.get_config()); } -fn assert_valid_serial_number(serial_number: &str) { - assert!(serial_number.is_ascii()); - assert!(serial_number.chars().all(|c| c.is_ascii_hexdigit())); -} - #[test_device] fn get_serial_number(device: DeviceWrapper) { let serial_number = unwrap_ok!(device.get_serial_number()); - assert_valid_serial_number(&serial_number); + assert!(serial_number != SerialNumber::empty()); } #[test_device] diff --git a/nitrokey/tests/pws.rs b/nitrokey/tests/pws.rs index 7169695..47e9703 100644 --- a/nitrokey/tests/pws.rs +++ b/nitrokey/tests/pws.rs @@ -16,7 +16,7 @@ use nitrokey_test::test as test_device; fn get_slot_name_direct(slot: u8) -> Result<String, Error> { let ptr = unsafe { nitrokey_sys::NK_get_password_safe_slot_name(slot) }; if ptr.is_null() { - return Err(Error::UnexpectedError); + return Err(Error::UnexpectedError("null pointer".to_owned())); } let s = unsafe { CStr::from_ptr(ptr).to_string_lossy().into_owned() }; unsafe { free(ptr as *mut c_void) }; |