// Copyright (C) 2018-2019 Robin Krahl // SPDX-License-Identifier: MIT mod pro; mod storage; mod wrapper; use std::convert::{TryFrom, TryInto}; use std::ffi; use std::fmt; use std::str; use crate::auth::Authenticate; use crate::config::{Config, RawConfig}; use crate::error::{CommunicationError, Error, LibraryError}; use crate::otp::GenerateOtp; use crate::pws::GetPasswordSafe; use crate::util::{ get_command_result, get_cstring, owned_str_from_ptr, result_or_error, run_with_string, }; pub use pro::Pro; pub use storage::{ OperationStatus, SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, }; pub use wrapper::DeviceWrapper; /// Available Nitrokey models. #[derive(Clone, Copy, Debug, PartialEq)] pub enum Model { /// The Nitrokey Storage. Storage, /// The Nitrokey Pro. Pro, } impl fmt::Display for Model { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(match *self { Model::Pro => "Pro", Model::Storage => "Storage", }) } } impl From for nitrokey_sys::NK_device_model { fn from(model: Model) -> Self { match model { Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE, Model::Pro => nitrokey_sys::NK_device_model_NK_PRO, } } } impl TryFrom for Model { type Error = Error; fn try_from(model: nitrokey_sys::NK_device_model) -> Result { match model { nitrokey_sys::NK_device_model_NK_DISCONNECTED => { Err(CommunicationError::NotConnected.into()) } nitrokey_sys::NK_device_model_NK_PRO => Ok(Model::Pro), nitrokey_sys::NK_device_model_NK_STORAGE => Ok(Model::Storage), _ => Err(Error::UnsupportedModelError), } } } /// 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, 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 { // 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 { /// The model of the Nitrokey device, or `None` if the model is not supported by this crate. pub model: Option, /// The USB device path. pub path: String, /// The serial number of the device, or `None` if the device does not expose its serial number. pub serial_number: Option, } impl TryFrom<&nitrokey_sys::NK_device_info> for DeviceInfo { type Error = Error; fn try_from(device_info: &nitrokey_sys::NK_device_info) -> Result { let model_result = device_info.model.try_into(); let model_option = model_result.map(Some).or_else(|err| match err { Error::UnsupportedModelError => Ok(None), _ => Err(err), })?; let serial_number = unsafe { ffi::CStr::from_ptr(device_info.serial_number) } .to_str() .map_err(Error::from)?; Ok(DeviceInfo { model: model_option, path: owned_str_from_ptr(device_info.path)?, serial_number: get_hidapi_serial_number(serial_number), }) } } impl fmt::Display for DeviceInfo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.model { Some(model) => write!(f, "Nitrokey {}", model)?, None => write!(f, "Unsupported Nitrokey model")?, } write!(f, " at {} with ", self.path)?; 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. /// /// 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. 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 { let len = serial_number.len(); if len < 8 { // The serial number in the USB descriptor has 12 bytes, we need at least four return None; } let mut iter = serial_number.char_indices().rev(); if let Some((i, _)) = iter.find(|(_, c)| *c != '0') { let substr = if len - i < 8 { // The last eight characters contain at least one non-zero character --> use them serial_number.split_at(len - 8).1 } else { // The last eight characters are all zero --> use the first eight serial_number.split_at(8).0 }; substr.parse().ok() } else { // The serial number is all zero None } } /// A firmware version for a Nitrokey device. #[derive(Clone, Copy, Debug, PartialEq)] pub struct FirmwareVersion { /// The major firmware version, e. g. 0 in v0.40. pub major: u8, /// The minor firmware version, e. g. 40 in v0.40. pub minor: u8, } impl fmt::Display for FirmwareVersion { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "v{}.{}", self.major, self.minor) } } /// The status information common to all Nitrokey devices. #[derive(Clone, Copy, Debug, PartialEq)] pub struct Status { /// The firmware version of the device. pub firmware_version: FirmwareVersion, /// The serial number of the device. pub serial_number: SerialNumber, /// The configuration of the device. pub config: Config, } impl From for Status { fn from(status: nitrokey_sys::NK_status) -> Self { Self { firmware_version: FirmwareVersion { major: status.firmware_version_major, minor: status.firmware_version_minor, }, serial_number: SerialNumber::new(status.serial_number_smart_card), config: RawConfig::from(&status).into(), } } } /// A Nitrokey device. /// /// This trait provides the commands that can be executed without authentication and that are /// present on all supported Nitrokey devices. pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt::Debug { /// Returns the [`Manager`][] instance that has been used to connect to this device. /// /// # Example /// /// ``` /// use nitrokey::{Device, DeviceWrapper}; /// /// fn do_something(device: DeviceWrapper) { /// // reconnect to any device /// let manager = device.into_manager(); /// let device = manager.connect(); /// // do something with the device /// // ... /// } /// /// match nitrokey::take()?.connect() { /// Ok(device) => do_something(device), /// Err(err) => println!("Could not connect to a Nitrokey: {}", err), /// } /// # Ok::<(), nitrokey::Error>(()) /// ``` fn into_manager(self) -> &'a mut crate::Manager; /// Returns the model of the connected Nitrokey device. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let device = manager.connect()?; /// println!("Connected to a Nitrokey {}", device.get_model()); /// # Ok(()) /// # } fn get_model(&self) -> Model; /// Returns the status of the Nitrokey device. /// /// This methods returns the status information common to all Nitrokey devices as a /// [`Status`][] struct. Some models may provide more information, for example /// [`get_storage_status`][] returns the [`StorageStatus`][] struct. /// /// # Errors /// /// - [`NotConnected`][] if the Nitrokey device has been disconnected /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// /// let mut manager = nitrokey::take()?; /// let device = manager.connect()?; /// let status = device.get_status()?; /// println!("Firmware version: {}", status.firmware_version); /// println!("Serial number: {}", status.serial_number); /// # Ok::<(), nitrokey::Error>(()) /// ``` /// /// [`get_storage_status`]: struct.Storage.html#method.get_storage_status /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected /// [`Status`]: struct.Status.html /// [`StorageStatus`]: struct.StorageStatus.html fn get_status(&self) -> Result; /// Returns the serial number of the Nitrokey device. /// /// For display purpuses, the serial number should be formatted as an 8-digit hex string. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let device = manager.connect()?; /// match device.get_serial_number() { /// Ok(number) => println!("serial no: {}", number), /// Err(err) => eprintln!("Could not get serial number: {}", err), /// }; /// # Ok(()) /// # } /// ``` fn get_serial_number(&self) -> Result { 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 /// available attempts is three. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let device = manager.connect()?; /// let count = device.get_user_retry_count(); /// match device.get_user_retry_count() { /// Ok(count) => println!("{} remaining authentication attempts (user)", count), /// Err(err) => eprintln!("Could not get user retry count: {}", err), /// } /// # Ok(()) /// # } /// ``` fn get_user_retry_count(&self) -> Result { result_or_error(unsafe { nitrokey_sys::NK_get_user_retry_count() }) } /// Returns the number of remaining authentication attempts for the admin. The total number of /// available attempts is three. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let device = manager.connect()?; /// let count = device.get_admin_retry_count(); /// match device.get_admin_retry_count() { /// Ok(count) => println!("{} remaining authentication attempts (admin)", count), /// Err(err) => eprintln!("Could not get admin retry count: {}", err), /// } /// # Ok(()) /// # } /// ``` fn get_admin_retry_count(&self) -> Result { result_or_error(unsafe { nitrokey_sys::NK_get_admin_retry_count() }) } /// Returns the firmware version. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let device = manager.connect()?; /// match device.get_firmware_version() { /// Ok(version) => println!("Firmware version: {}", version), /// Err(err) => eprintln!("Could not access firmware version: {}", err), /// }; /// # Ok(()) /// # } /// ``` fn get_firmware_version(&self) -> Result { let major = result_or_error(unsafe { nitrokey_sys::NK_get_major_firmware_version() })?; let minor = result_or_error(unsafe { nitrokey_sys::NK_get_minor_firmware_version() })?; Ok(FirmwareVersion { major, minor }) } /// Returns the current configuration of the Nitrokey device. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let device = manager.connect()?; /// let config = device.get_config()?; /// println!("numlock binding: {:?}", config.numlock); /// println!("capslock binding: {:?}", config.capslock); /// println!("scrollock binding: {:?}", config.scrollock); /// println!("require password for OTP: {:?}", config.user_password); /// # Ok(()) /// # } /// ``` fn get_config(&self) -> Result { 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. /// /// # Errors /// /// - [`InvalidString`][] if one of the provided passwords contains a null byte /// - [`WrongPassword`][] if the current admin password is wrong /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let mut device = manager.connect()?; /// match device.change_admin_pin("12345678", "12345679") { /// Ok(()) => println!("Updated admin PIN."), /// Err(err) => eprintln!("Failed to update admin PIN: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword fn change_admin_pin(&mut self, current: &str, new: &str) -> Result<(), Error> { let current_string = get_cstring(current)?; let new_string = get_cstring(new)?; get_command_result(unsafe { nitrokey_sys::NK_change_admin_PIN(current_string.as_ptr(), new_string.as_ptr()) }) } /// Changes the user PIN. /// /// # Errors /// /// - [`InvalidString`][] if one of the provided passwords contains a null byte /// - [`WrongPassword`][] if the current user password is wrong /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let mut device = manager.connect()?; /// match device.change_user_pin("123456", "123457") { /// Ok(()) => println!("Updated admin PIN."), /// Err(err) => eprintln!("Failed to update admin PIN: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword fn change_user_pin(&mut self, current: &str, new: &str) -> Result<(), Error> { let current_string = get_cstring(current)?; let new_string = get_cstring(new)?; get_command_result(unsafe { nitrokey_sys::NK_change_user_PIN(current_string.as_ptr(), new_string.as_ptr()) }) } /// Unlocks the user PIN after three failed login attempts and sets it to the given value. /// /// # Errors /// /// - [`InvalidString`][] if one of the provided passwords contains a null byte /// - [`WrongPassword`][] if the admin password is wrong /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let mut device = manager.connect()?; /// match device.unlock_user_pin("12345678", "123456") { /// Ok(()) => println!("Unlocked user PIN."), /// Err(err) => eprintln!("Failed to unlock user PIN: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword fn unlock_user_pin(&mut self, admin_pin: &str, user_pin: &str) -> Result<(), Error> { let admin_pin_string = get_cstring(admin_pin)?; let user_pin_string = get_cstring(user_pin)?; get_command_result(unsafe { nitrokey_sys::NK_unlock_user_password( admin_pin_string.as_ptr(), user_pin_string.as_ptr(), ) }) } /// Locks the Nitrokey device. /// /// This disables the password store if it has been unlocked. On the Nitrokey Storage, this /// also disables the volumes if they have been enabled. /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let mut device = manager.connect()?; /// match device.lock() { /// Ok(()) => println!("Locked the Nitrokey device."), /// Err(err) => eprintln!("Could not lock the Nitrokey device: {}", err), /// }; /// # Ok(()) /// # } /// ``` fn lock(&mut self) -> Result<(), Error> { get_command_result(unsafe { nitrokey_sys::NK_lock_device() }) } /// Performs a factory reset on the Nitrokey device. /// /// This commands performs a factory reset on the smart card (like the factory reset via `gpg /// --card-edit`) and then clears the flash memory (password safe, one-time passwords etc.). /// After a factory reset, [`build_aes_key`][] has to be called before the password safe or the /// encrypted volume can be used. /// /// # Errors /// /// - [`InvalidString`][] if the provided password contains a null byte /// - [`WrongPassword`][] if the admin password is wrong /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let mut device = manager.connect()?; /// match device.factory_reset("12345678") { /// Ok(()) => println!("Performed a factory reset."), /// Err(err) => eprintln!("Could not perform a factory reset: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`build_aes_key`]: #method.build_aes_key fn factory_reset(&mut self, admin_pin: &str) -> Result<(), Error> { let admin_pin_string = get_cstring(admin_pin)?; get_command_result(unsafe { nitrokey_sys::NK_factory_reset(admin_pin_string.as_ptr()) }) } /// Builds a new AES key on the Nitrokey. /// /// The AES key is used to encrypt the password safe and the encrypted volume. You may need /// to call this method after a factory reset, either using [`factory_reset`][] or using `gpg /// --card-edit`. You can also use it to destroy the data stored in the password safe or on /// the encrypted volume. /// /// # Errors /// /// - [`InvalidString`][] if the provided password contains a null byte /// - [`WrongPassword`][] if the admin password is wrong /// /// # Example /// /// ```no_run /// use nitrokey::Device; /// # use nitrokey::Error; /// /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let mut device = manager.connect()?; /// match device.build_aes_key("12345678") { /// Ok(()) => println!("New AES keys have been built."), /// Err(err) => eprintln!("Could not build new AES keys: {}", err), /// }; /// # Ok(()) /// # } /// ``` /// /// [`factory_reset`]: #method.factory_reset fn build_aes_key(&mut self, admin_pin: &str) -> Result<(), Error> { let admin_pin_string = get_cstring(admin_pin)?; get_command_result(unsafe { nitrokey_sys::NK_build_aes_key(admin_pin_string.as_ptr()) }) } } fn get_connected_model() -> Result { Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() }) } pub(crate) fn create_device_wrapper( manager: &mut crate::Manager, model: Model, ) -> DeviceWrapper<'_> { match model { Model::Pro => Pro::new(manager).into(), Model::Storage => Storage::new(manager).into(), } } pub(crate) fn get_connected_device( manager: &mut crate::Manager, ) -> Result, Error> { Ok(create_device_wrapper(manager, get_connected_model()?)) } pub(crate) fn connect_enum(model: Model) -> bool { unsafe { nitrokey_sys::NK_login_enum(model.into()) == 1 } } #[cfg(test)] mod tests { use std::str::FromStr; use super::{get_hidapi_serial_number, LibraryError, SerialNumber}; #[test] 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"); } }