summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md8
-rw-r--r--src/device/mod.rs108
-rw-r--r--src/device/storage.rs10
-rw-r--r--src/error.rs6
-rw-r--r--src/util.rs11
-rw-r--r--tests/device.rs13
-rw-r--r--tests/pws.rs2
7 files changed, 90 insertions, 68 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b75904c..f5bb7cb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,14 @@ Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>
SPDX-License-Identifier: CC0-1.0
-->
+# Unreleased
+- Add `String` value to the `Error::UnexpectedError` variant.
+- Always store serial numbers as integers:
+ - Change the return type of `Device::get_serial_number` from `Result<String,
+ _>` to `Result<u32, _>`.
+ - Change the type of the field `DeviceInfo.serial_number` from
+ `Option<String>` to `Option<u32>`.
+
# 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.
diff --git a/src/device/mod.rs b/src/device/mod.rs
index 0234bf0..a25ad1b 100644
--- a/src/device/mod.rs
+++ b/src/device/mod.rs
@@ -78,9 +78,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<u32>,
}
impl TryFrom<&nitrokey_sys::NK_device_info> for DeviceInfo {
@@ -110,45 +109,48 @@ 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. 0x{:08x}", serial_number),
None => write!(f, "an unknown serial number"),
}
}
}
-/// Parses a serial number returned by hidapi and transforms it to the Nitrokey format.
+/// Parses the given hex string and returns its integer representation.
+fn parse_serial_number<S: AsRef<str>>(s: S) -> Result<u32, Error> {
+ u32::from_str_radix(s.as_ref(), 16)
+ .map_err(|_| Error::UnexpectedError("Could not parse hex string".to_owned()))
+}
+
+/// Parses a serial number returned by hidapi and returns its integer value.
///
/// 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<u32> {
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
+ };
+ parse_serial_number(substr).ok()
} else {
// The serial number is all zero
None
@@ -263,7 +265,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: 0x{:08x}", status.serial_number);
/// # Ok::<(), nitrokey::Error>(())
/// ```
///
@@ -273,8 +275,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
///
@@ -286,14 +289,15 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt
/// let mut manager = nitrokey::take()?;
/// let device = manager.connect()?;
/// match device.get_serial_number() {
- /// Ok(number) => println!("serial no: {}", number),
+ /// Ok(number) => println!("serial no: 0x{:08x}", number),
/// Err(err) => eprintln!("Could not get serial number: {}", err),
/// };
/// # Ok(())
/// # }
/// ```
- fn get_serial_number(&self) -> Result<String, Error> {
+ fn get_serial_number(&self) -> Result<u32, Error> {
result_from_string(unsafe { nitrokey_sys::NK_device_serial_number() })
+ .and_then(parse_serial_number)
}
/// Returns the number of remaining authentication attempts for the user. The total number of
@@ -625,43 +629,55 @@ pub(crate) fn connect_enum(model: Model) -> bool {
#[cfg(test)]
mod tests {
- use super::get_hidapi_serial_number;
+ use super::{get_hidapi_serial_number, parse_serial_number};
+
+ #[test]
+ fn test_parse_serial_number() {
+ fn assert_err(s: &str) {
+ match parse_serial_number(s).unwrap_err() {
+ super::Error::UnexpectedError(_) => {}
+ err => assert!(false, "expected UnexpectedError, got {} (input {})", err, s),
+ }
+ }
+
+ assert_eq!(0x1234, parse_serial_number("1234").unwrap());
+ assert_eq!(0x1234, parse_serial_number("01234").unwrap());
+ assert_eq!(0x1234, parse_serial_number("001234").unwrap());
+ assert_eq!(0x1234, parse_serial_number("0001234").unwrap());
+
+ assert_eq!(0, parse_serial_number("0").unwrap());
+
+ assert_eq!(0xdeadbeef, parse_serial_number("deadbeef").unwrap());
+ assert_err("0xdeadbeef");
+ assert_err("deadpork");
+ assert_err("blubb");
+ assert_err("");
+ }
#[test]
- fn hidapi_serial_number() {
+ fn test_get_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("blubb"));
assert_eq!(None, get_hidapi_serial_number("1234"));
+ assert_eq!(Some(0x1234), get_hidapi_serial_number("00001234"));
+ assert_eq!(Some(0x1234), get_hidapi_serial_number("000000001234"));
+ assert_eq!(Some(0x1234), get_hidapi_serial_number("100000001234"));
+ assert_eq!(Some(0x12340000), get_hidapi_serial_number("123400000000"));
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()),
+ Some(0x5678),
get_hidapi_serial_number("000000000000000000005678")
);
assert_eq!(
- Some("00001234".to_string()),
+ Some(0x1234),
get_hidapi_serial_number("000012340000000000000000")
);
assert_eq!(
- Some("0000ffff".to_string()),
+ Some(0xffff),
get_hidapi_serial_number("00000000000000000000FFFF")
);
assert_eq!(
- Some("0000ffff".to_string()),
+ Some(0xffff),
get_hidapi_serial_number("00000000000000000000ffff")
);
}
diff --git a/src/device/storage.rs b/src/device/storage.rs
index deb2844..1e2c46d 100644
--- a/src/device/storage.rs
+++ b/src/device/storage.rs
@@ -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(),
+ )),
}
}
diff --git a/src/error.rs b/src/error.rs
index f9af594..7bea3f2 100644
--- a/src/error.rs
+++ b/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/src/util.rs b/src/util.rs
index a0d0d1b..08946d6 100644
--- a/src/util.rs
+++ b/src/util.rs
@@ -39,7 +39,9 @@ pub fn owned_str_from_ptr(ptr: *const c_char) -> Result<String, Error> {
pub fn result_from_string(ptr: *const c_char) -> Result<String, 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)?;
unsafe { free(ptr as *mut c_void) };
@@ -69,10 +71,9 @@ 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<CString, Error> {
diff --git a/tests/device.rs b/tests/device.rs
index a88c956..2989941 100644
--- a/tests/device.rs
+++ b/tests/device.rs
@@ -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 != 0);
}
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 != 0);
}
#[test_device]
diff --git a/tests/pws.rs b/tests/pws.rs
index 7169695..47e9703 100644
--- a/tests/pws.rs
+++ b/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) };