// 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 nitrokey_sys; use crate::auth::Authenticate; use crate::config::{Config, RawConfig}; use crate::error::{CommunicationError, Error}; use crate::otp::GenerateOtp; use crate::pws::GetPasswordSafe; use crate::util::{ get_command_result, get_cstring, owned_str_from_ptr, result_from_string, result_or_error, }; 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), } } } /// 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. 0x{:08x}", serial_number), None => write!(f, "an unknown serial number"), } } } /// Parses the given hex string and returns its integer representation. fn parse_serial_number>(s: S) -> Result { 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. 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 iter = serial_number.char_indices().rev(); let first_non_null = iter.skip_while(|(_, c)| *c == '0').next(); if let Some((i, _)) = first_non_null { 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 }; parse_serial_number(substr).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: u32, /// 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: 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: 0x{:08x}", 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: 0x{:08x}", number), /// Err(err) => eprintln!("Could not get serial number: {}", err), /// }; /// # Ok(()) /// # } /// ``` fn get_serial_number(&self) -> Result { 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 /// 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 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 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(0x5678), get_hidapi_serial_number("000000000000000000005678") ); assert_eq!( Some(0x1234), get_hidapi_serial_number("000012340000000000000000") ); assert_eq!( Some(0xffff), get_hidapi_serial_number("00000000000000000000FFFF") ); assert_eq!( Some(0xffff), get_hidapi_serial_number("00000000000000000000ffff") ); } }