diff options
-rw-r--r-- | CHANGELOG.md | 19 | ||||
-rw-r--r-- | Cargo.toml | 2 | ||||
-rw-r--r-- | README.md | 42 | ||||
-rw-r--r-- | TODO.md | 16 | ||||
-rw-r--r-- | examples/list-devices.rs | 26 | ||||
-rw-r--r-- | examples/otp.rs | 43 | ||||
-rw-r--r-- | src/device/mod.rs | 209 | ||||
-rw-r--r-- | src/device/pro.rs | 18 | ||||
-rw-r--r-- | src/device/storage.rs | 161 | ||||
-rw-r--r-- | src/device/wrapper.rs | 9 | ||||
-rw-r--r-- | src/error.rs | 4 | ||||
-rw-r--r-- | src/lib.rs | 136 | ||||
-rw-r--r-- | tests/device.rs | 158 |
13 files changed, 778 insertions, 65 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index d4451bc..54ee7d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,25 @@ Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org> SPDX-License-Identifier: CC0-1.0 --> +# v0.5.0 (2020-01-14) +- List these libnitrokey functions as unsupported: + - `NK_change_firmware_password_pro` + - `NK_connect_with_ID` + - `NK_enable_firmware_update_pro` + - `NK_list_devices_by_cpuID` + - `NK_send_startup` +- Implement connection by path: + - Add the `Error::UnsupportedDeviceError` variant. + - Add the `DeviceInfo` struct. + - Add the `list_devices` function. + - Add the `connect_path` function to the `Manager` struct. +- Add the `get_status` function to the `Device` trait. +- Rename `Status::get_status` to `get_storage_status`. +- Add the `get_sd_card_usage` function to the `Storage` struct. +- Add the `OperationStatus` enum and the `get_operation_status` function for + the `Storage` struct. +- Add the `fill_sd_card` function to the `Storage` struct. + # v0.4.0 (2020-01-02) - Remove the `test-pro` and `test-storage` features. - Implement `Display` for `Version`. @@ -3,7 +3,7 @@ [package] name = "nitrokey" -version = "0.4.0" +version = "0.5.0" authors = ["Robin Krahl <robin.krahl@ireas.org>"] edition = "2018" homepage = "https://code.ireas.org/nitrokey-rs/" @@ -7,32 +7,48 @@ SPDX-License-Identifier: CC0-1.0 A libnitrokey wrapper for Rust providing access to Nitrokey devices. -[Documentation][] +## Usage + +For usage information, have a look at the [API reference][API reference] and at +the [examples][] in the `examples` directory. You can also have a look at the +[`nitrocli`][] crate, a command-line interface for Nitrokey devices that uses +this crate. ## Compatibility -The required [`libnitrokey`][] version is built from source. The host system +This crate provides access to all features of the [`libnitrokey`][] C API for +both the Nitrokey Pro and the Nitrokey Storage: general configuration, one-time +password generation, the password safe and the secure storage on the Nitrokey +Storage. + +The required `libnitrokey` version is built from source. The host system must provide `libhidapi-libusb0` (Linux) or `libhidapi` (non-Linux) in the default library search path. Depending on your system, you might also have to install the [Nitrokey udev rules][]. -Currently, this crate provides access to the common features of the Nitrokey -Pro and the Nitrokey Storage: general configuration, OTP generation and the -password safe. Basic support for the secure storage on the Nitrokey Storage is -available but still under development. +If you want to use a precompiled version of `libnitrokey`, you can set the +`USE_SYSTEM_LIBNITROKEY` environment variable during build. In this case, +`libnitrokey` must be available in the library search path. ### Unsupported Functions The following functions provided by `libnitrokey` are deliberately not supported by `nitrokey-rs`: +- `NK_connect_with_ID`, `NK_list_devices_by_cpuID`. These functions can be + replaced by calls to `NK_connect_with_path` and `NK_list_devices`, which + also have a cleaner API. +- `NK_enable_firmware_update_pro`, `NK_change_firmware_password_pro`. These + functions execute commands that are not yet supported by the Nitrokey Pro + firmware. - `NK_get_device_model`. We know which model we connected to, so we can provide this information without calling `libnitrokey`. -- `NK_is_AES_supported`. This method is no longer needed for Nitrokey devices - with a recent firmware version. +- `NK_is_AES_supported`. This function is no longer needed for Nitrokey + devices with a recent firmware version. +- `NK_send_startup`. Currently, this function is redundant to `NK_get_time`. - `NK_set_unencrypted_volume_rorw_pin_type_user`, `NK_set_unencrypted_read_only`, `NK_set_unencrypted_read_write`. These - methods are only relevant for older firmware versions (pre-v0.51). As the + functions are only relevant for older firmware versions (pre-v0.51). As the Nitrokey Storage firmware can be updated easily, we do not support these outdated versions. - `NK_totp_get_time`, `NK_status`. These functions are deprecated. @@ -57,6 +73,10 @@ an AES key has been built. Some tests will overwrite the data stored on the Nitrokey device or perform a factory reset. Never execute the tests if you don’t want to destroy all data on any connected Nitrokey device! +The test suite contains some test that take very long to execute, for example +filling the SD card of a Nitrokey Storage with random data. These tests are +ignored per default. Use `cargo test -- --ignored` to execute the tests. + ## Acknowledgments Thanks to Nitrokey UG for providing two Nitrokey devices to support the @@ -81,7 +101,9 @@ in the `LICENSES` directory. `libnitrokey` is licensed under the [LGPL-3.0][]. `nitrokey-rs` complies with [version 3.0 of the REUSE specification][reuse]. -[Documentation]: https://docs.rs/nitrokey +[API reference]: https://docs.rs/nitrokey +[examples]: https://docs.rs/crate/nitrokey/0.4.0/source/examples/ +[`nitrocli`]: https://github.com/d-e-s-o/nitrocli/tree/master/nitrocli [Nitrokey udev rules]: https://www.nitrokey.com/documentation/frequently-asked-questions-faq#openpgp-card-not-available [`libnitrokey`]: https://github.com/nitrokey/libnitrokey [`nitrokey-test`]: https://github.com/d-e-s-o/nitrokey-test @@ -3,24 +3,8 @@ Copyright (C) 2019 Robin Krahl <robin.krahl@ireas.org> SPDX-License-Identifier: CC0-1.0 --> -- Add support for the currently unsupported commands: - - `NK_send_startup` - - `NK_fill_SD_card_with_random_data` - - `NK_get_SD_usage_data` - - `NK_get_progress_bar_value` - - `NK_list_devices_by_cpuID` - - `NK_connect_with_ID` - - `NK_get_status` - - `NK_list_devices` - - `NK_free_device_info` - - `NK_connect_with_path` - - `NK_enable_firmware_update_pro` - - `NK_change_firmware_password_pro` - Clear passwords from memory. - Lock password safe in `PasswordSafe::drop()` (see [nitrokey-storage-firmware issue 65][]). -- Disable creation of multiple password safes at the same time. -- Check timing in Storage tests. -- Consider restructuring `device::StorageStatus`. [nitrokey-storage-firmware issue 65]: https://github.com/Nitrokey/nitrokey-storage-firmware/issues/65 diff --git a/examples/list-devices.rs b/examples/list-devices.rs new file mode 100644 index 0000000..9c14533 --- /dev/null +++ b/examples/list-devices.rs @@ -0,0 +1,26 @@ +// Copyright (C) 2020 Robin Krahl <robin.krahl@ireas.org> +// SPDX-License-Identifier: CC-0 + +//! Enumerates all connected Nitrokey devices and prints some information about them. + +use nitrokey::Device as _; + +fn main() -> Result<(), nitrokey::Error> { + let mut manager = nitrokey::take()?; + let device_infos = nitrokey::list_devices()?; + if device_infos.is_empty() { + println!("No Nitrokey device found"); + } else { + println!("path\t\tmodel\tfirmware version\tserial number"); + for device_info in device_infos { + let device = manager.connect_path(device_info.path.clone())?; + let model = device.get_model(); + let status = device.get_status()?; + println!( + "{}\t{}\t{}\t\t\t{:08x}", + device_info.path, model, status.firmware_version, status.serial_number + ); + } + } + Ok(()) +} diff --git a/examples/otp.rs b/examples/otp.rs new file mode 100644 index 0000000..819de28 --- /dev/null +++ b/examples/otp.rs @@ -0,0 +1,43 @@ +// Copyright (C) 2020 Robin Krahl <robin.krahl@ireas.org> +// SPDX-License-Identifier: CC-0 + +//! Connects to a Nitrokey device, configures an TOTP slot and generates a one-time password from +//! it. + +use std::time; + +use nitrokey::{Authenticate, ConfigureOtp, Device, GenerateOtp}; + +fn main() -> Result<(), nitrokey::Error> { + let mut manager = nitrokey::take()?; + let device = manager.connect()?; + + // Configure the OTP slot (requires admin PIN) + let data = nitrokey::OtpSlotData::new( + 1, + "test", + "3132333435363738393031323334353637383930", + nitrokey::OtpMode::SixDigits, + ); + let mut admin = device.authenticate_admin("12345678")?; + admin.write_totp_slot(data, 30)?; + let mut device = admin.device(); + + // Set the time for the OTP generation + let time = time::SystemTime::now() + .duration_since(time::UNIX_EPOCH) + .expect("Invalid system time"); + device.set_time(time.as_secs(), true)?; + + // Generate a one-time password -- depending on the configuration, we have to set the user PIN + let config = device.get_config()?; + let otp = if config.user_password { + let user = device.authenticate_user("123456")?; + user.get_totp_code(1) + } else { + device.get_totp_code(1) + }?; + println!("Generated OTP code: {}", otp); + + Ok(()) +} diff --git a/src/device/mod.rs b/src/device/mod.rs index 5e15f08..16d6b11 100644 --- a/src/device/mod.rs +++ b/src/device/mod.rs @@ -5,6 +5,9 @@ mod pro; mod storage; mod wrapper; +use std::cmp; +use std::convert::{TryFrom, TryInto}; +use std::ffi; use std::fmt; use libc; @@ -16,12 +19,14 @@ use crate::error::{CommunicationError, Error}; use crate::otp::GenerateOtp; use crate::pws::GetPasswordSafe; use crate::util::{ - get_command_result, get_cstring, get_last_error, result_from_string, result_or_error, + get_command_result, get_cstring, get_last_error, owned_str_from_ptr, result_from_string, + result_or_error, }; pub use pro::Pro; pub use storage::{ - SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, + OperationStatus, SdCardData, Storage, StorageProductionInfo, StorageStatus, VolumeMode, + VolumeStatus, }; pub use wrapper::DeviceWrapper; @@ -43,6 +48,97 @@ impl fmt::Display for Model { } } +impl From<Model> 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<nitrokey_sys::NK_device_model> for Model { + type Error = Error; + + fn try_from(model: nitrokey_sys::NK_device_model) -> Result<Self, Error> { + 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<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>, +} + +impl TryFrom<&nitrokey_sys::NK_device_info> for DeviceInfo { + type Error = Error; + + fn try_from(device_info: &nitrokey_sys::NK_device_info) -> Result<DeviceInfo, Error> { + 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(ref 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. +/// +/// If the serial number is all zero, this function returns `None`. Otherwise, all leading zeros +/// (except the last eight characters) are stripped and `Some(_)` is returned. If the serial +/// number has less than eight characters, leading zeros are added. +fn get_hidapi_serial_number(serial_number: &str) -> Option<String> { + let mut iter = serial_number.char_indices().skip_while(|(_, c)| *c == '0'); + let first_non_null = iter.next(); + if let Some((i, _)) = first_non_null { + // keep at least the last 8 characters + let len = serial_number.len(); + let cut = cmp::min(len.saturating_sub(8), i); + let (_, suffix) = serial_number.split_at(cut); + // if necessary, add leading zeros to reach 8 characters + let fill = 8usize.saturating_sub(len); + Some("0".repeat(fill) + suffix) + } else { + None + } +} + /// A firmware version for a Nitrokey device. #[derive(Clone, Copy, Debug, PartialEq)] pub struct FirmwareVersion { @@ -58,6 +154,36 @@ impl fmt::Display for FirmwareVersion { } } +/// 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<nitrokey_sys::NK_status> 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 { + numlock: status.config_numlock, + capslock: status.config_capslock, + scrollock: status.config_scrolllock, + user_password: status.otp_user_password, + } + .into(), + } + } +} + /// A Nitrokey device. /// /// This trait provides the commands that can be executed without authentication and that are @@ -102,6 +228,35 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt /// # } 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: {:x}", 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<Status, Error>; + /// Returns the serial number of the Nitrokey device. The serial number is the string /// representation of a hex number. /// @@ -428,12 +583,8 @@ pub trait Device<'a>: Authenticate<'a> + GetPasswordSafe<'a> + GenerateOtp + fmt } } -fn get_connected_model() -> Option<Model> { - match unsafe { nitrokey_sys::NK_get_device_model() } { - nitrokey_sys::NK_device_model_NK_PRO => Some(Model::Pro), - nitrokey_sys::NK_device_model_NK_STORAGE => Some(Model::Storage), - _ => None, - } +fn get_connected_model() -> Result<Model, Error> { + Model::try_from(unsafe { nitrokey_sys::NK_get_device_model() }) } pub(crate) fn create_device_wrapper( @@ -449,16 +600,40 @@ pub(crate) fn create_device_wrapper( pub(crate) fn get_connected_device( manager: &mut crate::Manager, ) -> Result<DeviceWrapper<'_>, Error> { - match get_connected_model() { - Some(model) => Ok(create_device_wrapper(manager, model)), - None => Err(CommunicationError::NotConnected.into()), - } + Ok(create_device_wrapper(manager, get_connected_model()?)) } pub(crate) fn connect_enum(model: Model) -> bool { - let model = match model { - Model::Storage => nitrokey_sys::NK_device_model_NK_STORAGE, - Model::Pro => nitrokey_sys::NK_device_model_NK_PRO, - }; - unsafe { nitrokey_sys::NK_login_enum(model) == 1 } + unsafe { nitrokey_sys::NK_login_enum(model.into()) == 1 } +} + +#[cfg(test)] +mod tests { + use super::get_hidapi_serial_number; + + #[test] + fn hidapi_serial_number() { + assert_eq!(None, get_hidapi_serial_number("")); + assert_eq!(None, get_hidapi_serial_number("00000000000000000")); + assert_eq!( + Some("00001234".to_string()), + 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("100000001234".to_string()), + get_hidapi_serial_number("100000001234") + ); + assert_eq!( + Some("10000001234".to_string()), + get_hidapi_serial_number("010000001234") + ); + } } diff --git a/src/device/pro.rs b/src/device/pro.rs index a65345e..591b730 100644 --- a/src/device/pro.rs +++ b/src/device/pro.rs @@ -3,8 +3,10 @@ use nitrokey_sys; -use crate::device::{Device, Model}; +use crate::device::{Device, Model, Status}; +use crate::error::Error; use crate::otp::GenerateOtp; +use crate::util::get_command_result; /// A Nitrokey Pro device without user or admin authentication. /// @@ -74,6 +76,20 @@ impl<'a> Device<'a> for Pro<'a> { fn get_model(&self) -> Model { Model::Pro } + + fn get_status(&self) -> Result<Status, Error> { + 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(raw_status.into()) + } } impl<'a> GenerateOtp for Pro<'a> {} diff --git a/src/device/storage.rs b/src/device/storage.rs index 370ce36..deb2844 100644 --- a/src/device/storage.rs +++ b/src/device/storage.rs @@ -1,14 +1,16 @@ -// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org> +// Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org> // SPDX-License-Identifier: MIT +use std::convert::TryFrom as _; use std::fmt; +use std::ops; use nitrokey_sys; -use crate::device::{Device, FirmwareVersion, Model}; -use crate::error::Error; +use crate::device::{Device, FirmwareVersion, Model, Status}; +use crate::error::{CommandError, Error}; use crate::otp::GenerateOtp; -use crate::util::{get_command_result, get_cstring}; +use crate::util::{get_command_result, get_cstring, get_last_error}; /// A Nitrokey Storage device without user or admin authentication. /// @@ -141,6 +143,20 @@ pub struct StorageStatus { pub stick_initialized: bool, } +/// The progress of a background operation on the Nitrokey. +/// +/// Some commands may start a background operation during which no other commands can be executed. +/// This enum stores the status of a background operation: Ongoing with a relative progress (up to +/// 100), or idle, i. e. no background operation has been started or the last one has been +/// finished. +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum OperationStatus { + /// A background operation with its progress value (less than or equal to 100). + Ongoing(u8), + /// No backgrund operation. + Idle, +} + impl<'a> Storage<'a> { pub(crate) fn new(manager: &'a mut crate::Manager) -> Storage<'a> { Storage { @@ -525,7 +541,7 @@ impl<'a> Storage<'a> { /// # fn try_main() -> Result<(), Error> { /// let mut manager = nitrokey::take()?; /// let device = manager.connect_storage()?; - /// match device.get_status() { + /// match device.get_storage_status() { /// Ok(status) => { /// println!("SD card ID: {:#x}", status.serial_number_sd_card); /// }, @@ -534,7 +550,7 @@ impl<'a> Storage<'a> { /// # Ok(()) /// # } /// ``` - pub fn get_status(&self) -> Result<StorageStatus, Error> { + pub fn get_storage_status(&self) -> Result<StorageStatus, Error> { let mut raw_status = nitrokey_sys::NK_storage_status { unencrypted_volume_read_only: false, unencrypted_volume_active: false, @@ -635,11 +651,117 @@ impl<'a> Storage<'a> { }) } + /// Returns a range of the SD card that has not been used to during this power cycle. + /// + /// The Nitrokey Storage tracks read and write access to the SD card during a power cycle. + /// This method returns a range of the SD card that has not been accessed during this power + /// cycle. The range is relative to the total size of the SD card, so both values are less + /// than or equal to 100. This can be used as a guideline when creating a hidden volume. + /// + /// # Example + /// + /// ```no_run + /// let mut manager = nitrokey::take()?; + /// let storage = manager.connect_storage()?; + /// let usage = storage.get_sd_card_usage()?; + /// println!("SD card usage: {}..{}", usage.start, usage.end); + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + pub fn get_sd_card_usage(&self) -> Result<ops::Range<u8>, Error> { + let mut usage_data = nitrokey_sys::NK_SD_usage_data { + write_level_min: 0, + write_level_max: 0, + }; + let result = unsafe { nitrokey_sys::NK_get_SD_usage_data(&mut usage_data) }; + match get_command_result(result) { + Ok(_) => { + if usage_data.write_level_min > usage_data.write_level_max + || usage_data.write_level_max > 100 + { + Err(Error::UnexpectedError) + } else { + Ok(ops::Range { + start: usage_data.write_level_min, + end: usage_data.write_level_max, + }) + } + } + Err(err) => Err(err), + } + } + /// Blinks the red and green LED alternatively and infinitely until the device is reconnected. pub fn wink(&mut self) -> Result<(), Error> { get_command_result(unsafe { nitrokey_sys::NK_wink() }) } + /// Returns the status of an ongoing background operation on the Nitrokey Storage. + /// + /// Some commands may start a background operation during which no other commands can be + /// executed. This method can be used to check whether such an operation is ongoing. + /// + /// Currently, this is only used by the [`fill_sd_card`][] method. + /// + /// [`fill_sd_card`]: #method.fill_sd_card + pub fn get_operation_status(&self) -> Result<OperationStatus, Error> { + let status = unsafe { nitrokey_sys::NK_get_progress_bar_value() }; + match status { + 0..=100 => u8::try_from(status) + .map(OperationStatus::Ongoing) + .map_err(|_| Error::UnexpectedError), + -1 => Ok(OperationStatus::Idle), + -2 => Err(get_last_error()), + _ => Err(Error::UnexpectedError), + } + } + + /// Overwrites the SD card with random data. + /// + /// Ths method starts a background operation that overwrites the SD card with random data. + /// While this operation is ongoing, no other commands can be executed. Use the + /// [`get_operation_status`][] function to check the progress of the operation. + /// + /// # Errors + /// + /// - [`InvalidString`][] if one of the provided passwords contains a null byte + /// - [`WrongPassword`][] if the admin password is wrong + /// + /// # Example + /// + /// ```no_run + /// use nitrokey::OperationStatus; + /// + /// let mut manager = nitrokey::take()?; + /// let mut storage = manager.connect_storage()?; + /// storage.fill_sd_card("12345678")?; + /// loop { + /// match storage.get_operation_status()? { + /// OperationStatus::Ongoing(progress) => println!("{}/100", progress), + /// OperationStatus::Idle => { + /// println!("Done!"); + /// break; + /// } + /// } + /// } + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + /// + /// [`get_operation_status`]: #method.get_operation_status + /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString + /// [`WrongPassword`]: enum.CommandError.html#variant.WrongPassword + pub fn fill_sd_card(&mut self, admin_pin: &str) -> Result<(), Error> { + let admin_pin_string = get_cstring(admin_pin)?; + get_command_result(unsafe { + nitrokey_sys::NK_fill_SD_card_with_random_data(admin_pin_string.as_ptr()) + }) + .or_else(|err| match err { + // libnitrokey’s C API returns a LongOperationInProgressException with the same error + // code as the WrongCrc command error, so we cannot distinguish them. + Error::CommandError(CommandError::WrongCrc) => Ok(()), + err => Err(err), + }) + } + /// Exports the firmware to the unencrypted volume. /// /// This command requires the admin PIN. The unencrypted volume must be in read-write mode @@ -678,6 +800,33 @@ impl<'a> Device<'a> for Storage<'a> { fn get_model(&self) -> Model { Model::Storage } + + fn get_status(&self) -> Result<Status, Error> { + // Currently, the GET_STATUS command does not report the correct firmware version and + // serial number on the Nitrokey Storage, see [0]. Until this is fixed in libnitrokey, we + // have to manually execute the GET_DEVICE_STATUS command (get_storage_status) and complete + // the missing data, see [1]. + // [0] https://github.com/Nitrokey/nitrokey-storage-firmware/issues/96 + // [1] https://github.com/Nitrokey/libnitrokey/issues/166 + + 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) })?; + let mut status = Status::from(raw_status); + + let storage_status = self.get_storage_status()?; + status.firmware_version = storage_status.firmware_version; + status.serial_number = storage_status.serial_number_smart_card; + + Ok(status) + } } impl<'a> GenerateOtp for Storage<'a> {} diff --git a/src/device/wrapper.rs b/src/device/wrapper.rs index a3a18f9..69291ad 100644 --- a/src/device/wrapper.rs +++ b/src/device/wrapper.rs @@ -1,7 +1,7 @@ // Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org> // SPDX-License-Identifier: MIT -use crate::device::{Device, Model, Pro, Storage}; +use crate::device::{Device, Model, Pro, Status, Storage}; use crate::error::Error; use crate::otp::GenerateOtp; @@ -131,4 +131,11 @@ impl<'a> Device<'a> for DeviceWrapper<'a> { DeviceWrapper::Storage(_) => Model::Storage, } } + + fn get_status(&self) -> Result<Status, Error> { + match self { + DeviceWrapper::Pro(dev) => dev.get_status(), + DeviceWrapper::Storage(dev) => dev.get_status(), + } + } } diff --git a/src/error.rs b/src/error.rs index 9e6adc0..f9af594 100644 --- a/src/error.rs +++ b/src/error.rs @@ -28,6 +28,8 @@ pub enum Error { UnexpectedError, /// An unknown error returned by libnitrokey. UnknownError(i64), + /// An error caused by a Nitrokey model that is not supported by this crate. + UnsupportedModelError, /// An error occurred when interpreting a UTF-8 string. Utf8Error(str::Utf8Error), } @@ -102,6 +104,7 @@ impl error::Error for Error { Error::RandError(ref err) => Some(err.as_ref()), Error::UnexpectedError => None, Error::UnknownError(_) => None, + Error::UnsupportedModelError => None, Error::Utf8Error(ref err) => Some(err), } } @@ -118,6 +121,7 @@ impl fmt::Display for Error { Error::RandError(ref err) => write!(f, "RNG error: {}", err), Error::UnexpectedError => write!(f, "An unexpected error occurred"), 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), } } @@ -16,6 +16,10 @@ //! [`connect_model`][], [`connect_pro`][] or [`connect_storage`][] to connect to a specific //! device. //! +//! To get a list of all connected Nitrokey devices, use the [`list_devices`][] function. You can +//! then connect to one of the connected devices using the [`connect_path`][] function of the +//! `Manager` struct. +//! //! You can call [`authenticate_user`][] or [`authenticate_admin`][] to get an authenticated device //! that can perform operations that require authentication. You can use [`device`][] to go back //! to the unauthenticated device. @@ -25,6 +29,15 @@ //! passwords – [`get_hotp_code`][] and [`get_totp_code`][]. Depending on the stick configuration, //! these operations are available without authentication or with user authentication. //! +//! # Background operations +//! +//! Some commands may start background operations. During such an operation, every new command +//! will cause a [`WrongCrc`][] error. To check whether a background operation is currently +//! running, use the [`get_operation_status`][] method. +//! +//! Background operations are only available on the Nitrokey Storage. Currently, +//! [`fill_sd_card`][] is the only command that triggers a background operation. +//! //! # Examples //! //! Connect to any Nitrokey and print its serial number: @@ -86,8 +99,12 @@ //! [`take`]: fn.take.html //! [`connect`]: struct.Manager.html#method.connect //! [`connect_model`]: struct.Manager.html#method.connect_model +//! [`connect_path`]: struct.Manager.html#method.connect_path //! [`connect_pro`]: struct.Manager.html#method.connect_pro //! [`connect_storage`]: struct.Manager.html#method.connect_storage +//! [`fill_sd_card`]: struct.Storage.html#method.fill_sd_card +//! [`get_operation_status`]: struct.Storage.html#method.get_operation_status +//! [`list_devices`]: fn.list_devices.html //! [`manager`]: trait.Device.html#method.manager //! [`device`]: struct.User.html#method.device //! [`get_hotp_code`]: trait.GenerateOtp.html#method.get_hotp_code @@ -95,6 +112,7 @@ //! [`Admin`]: struct.Admin.html //! [`DeviceWrapper`]: enum.DeviceWrapper.html //! [`User`]: struct.User.html +//! [`WrongCrc`]: enum.CommandError.html#variant.WrongCrc #![warn(missing_docs, rust_2018_compatibility, rust_2018_idioms, unused)] @@ -109,8 +127,10 @@ mod otp; mod pws; mod util; +use std::convert::TryInto as _; use std::fmt; use std::marker; +use std::ptr::NonNull; use std::sync; use nitrokey_sys; @@ -118,14 +138,16 @@ use nitrokey_sys; pub use crate::auth::{Admin, Authenticate, User}; pub use crate::config::Config; pub use crate::device::{ - Device, DeviceWrapper, Model, Pro, SdCardData, Storage, StorageProductionInfo, StorageStatus, - VolumeMode, VolumeStatus, + Device, DeviceInfo, DeviceWrapper, Model, OperationStatus, Pro, SdCardData, Status, Storage, + StorageProductionInfo, StorageStatus, VolumeMode, VolumeStatus, }; pub use crate::error::{CommandError, CommunicationError, Error, LibraryError}; pub use crate::otp::{ConfigureOtp, GenerateOtp, OtpMode, OtpSlotData}; pub use crate::pws::{GetPasswordSafe, PasswordSafe, SLOT_COUNT}; pub use crate::util::LogLevel; +use crate::util::{get_cstring, get_last_result}; + /// The default admin PIN for all Nitrokey devices. pub const DEFAULT_ADMIN_PIN: &str = "12345678"; /// The default user PIN for all Nitrokey devices. @@ -235,6 +257,7 @@ impl Manager { /// # Errors /// /// - [`NotConnected`][] if no Nitrokey device is connected + /// - [`UnsupportedModelError`][] if the Nitrokey device is not supported by this crate /// /// # Example /// @@ -252,6 +275,7 @@ impl Manager { /// ``` /// /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + /// [`UnsupportedModelError`]: enum.Error.html#variant.UnsupportedModelError pub fn connect(&mut self) -> Result<DeviceWrapper<'_>, Error> { if unsafe { nitrokey_sys::NK_login_auto() } == 1 { device::get_connected_device(self) @@ -290,6 +314,49 @@ impl Manager { } } + /// Connects to a Nitrokey device at the given USB path. + /// + /// To get a list of all connected Nitrokey devices, use the [`list_devices`][] function. The + /// [`DeviceInfo`][] structs returned by that function contain the USB path in the `path` + /// field. + /// + /// # Errors + /// + /// - [`InvalidString`][] if the USB path contains a null byte + /// - [`NotConnected`][] if no Nitrokey device can be found at the given USB path + /// - [`UnsupportedModelError`][] if the model of the Nitrokey device at the given USB path is + /// not supported by this crate + /// + /// # Example + /// + /// ``` + /// use nitrokey::DeviceWrapper; + /// + /// fn use_device(device: DeviceWrapper) {} + /// + /// let mut manager = nitrokey::take()?; + /// let devices = nitrokey::list_devices()?; + /// for device in devices { + /// let device = manager.connect_path(device.path)?; + /// use_device(device); + /// } + /// # Ok::<(), nitrokey::Error>(()) + /// ``` + /// + /// [`list_devices`]: fn.list_devices.html + /// [`DeviceInfo`]: struct.DeviceInfo.html + /// [`InvalidString`]: enum.LibraryError.html#variant.InvalidString + /// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected + /// [`UnsupportedModelError`]: enum.Error.html#variant.UnsupportedModelError + pub fn connect_path<S: Into<Vec<u8>>>(&mut self, path: S) -> Result<DeviceWrapper<'_>, Error> { + let path = get_cstring(path)?; + if unsafe { nitrokey_sys::NK_connect_with_path(path.as_ptr()) } == 1 { + device::get_connected_device(self) + } else { + Err(CommunicationError::NotConnected.into()) + } + } + /// Connects to a Nitrokey Pro. /// /// # Errors @@ -414,6 +481,71 @@ pub fn force_take() -> Result<sync::MutexGuard<'static, Manager>, Error> { } } +/// List all connected Nitrokey devices. +/// +/// This functions returns a vector with [`DeviceInfo`][] structs that contain information about +/// all connected Nitrokey devices. It will even list unsupported models, although you cannot +/// connect to them. To connect to a supported model, call the [`connect_path`][] function. +/// +/// # Errors +/// +/// - [`NotConnected`][] if a Nitrokey device has been disconnected during enumeration +/// - [`Utf8Error`][] if the USB path or the serial number returned by libnitrokey are invalid +/// UTF-8 strings +/// +/// # Example +/// +/// ``` +/// let devices = nitrokey::list_devices()?; +/// if devices.is_empty() { +/// println!("No connected Nitrokey devices found."); +/// } else { +/// println!("model\tpath\tserial number"); +/// for device in devices { +/// match device.model { +/// Some(model) => print!("{}", model), +/// None => print!("unsupported"), +/// } +/// print!("\t{}\t", device.path); +/// match device.serial_number { +/// Some(serial_number) => println!("{}", serial_number), +/// None => println!("unknown"), +/// } +/// } +/// } +/// # Ok::<(), nitrokey::Error>(()) +/// ``` +/// +/// [`connect_path`]: struct.Manager.html#fn.connect_path +/// [`DeviceInfo`]: struct.DeviceInfo.html +/// [`NotConnected`]: enum.CommunicationError.html#variant.NotConnected +/// [`Utf8Error`]: enum.Error.html#variant.Utf8Error +pub fn list_devices() -> Result<Vec<DeviceInfo>, Error> { + let ptr = NonNull::new(unsafe { nitrokey_sys::NK_list_devices() }); + match ptr { + Some(mut ptr) => { + let mut vec: Vec<DeviceInfo> = Vec::new(); + push_device_info(&mut vec, unsafe { ptr.as_ref() })?; + unsafe { + nitrokey_sys::NK_free_device_info(ptr.as_mut()); + } + Ok(vec) + } + None => get_last_result().map(|_| Vec::new()), + } +} + +fn push_device_info( + vec: &mut Vec<DeviceInfo>, + info: &nitrokey_sys::NK_device_info, +) -> Result<(), Error> { + vec.push(info.try_into()?); + if let Some(ptr) = NonNull::new(info.next) { + push_device_info(vec, unsafe { ptr.as_ref() })?; + } + Ok(()) +} + /// Enables or disables debug output. Calling this method with `true` is equivalent to setting the /// log level to `Debug`; calling it with `false` is equivalent to the log level `Error` (see /// [`set_log_level`][]). diff --git a/tests/device.rs b/tests/device.rs index e367558..a88c956 100644 --- a/tests/device.rs +++ b/tests/device.rs @@ -1,4 +1,4 @@ -// Copyright (C) 2018-2019 Robin Krahl <robin.krahl@ireas.org> +// Copyright (C) 2018-2020 Robin Krahl <robin.krahl@ireas.org> // SPDX-License-Identifier: MIT mod util; @@ -8,9 +8,9 @@ use std::process::Command; use std::{thread, time}; use nitrokey::{ - Authenticate, CommandError, CommunicationError, Config, ConfigureOtp, Device, Error, - GenerateOtp, GetPasswordSafe, LibraryError, OtpMode, OtpSlotData, Storage, VolumeMode, - DEFAULT_ADMIN_PIN, DEFAULT_USER_PIN, + Authenticate, CommandError, CommunicationError, Config, ConfigureOtp, Device, DeviceInfo, + Error, GenerateOtp, GetPasswordSafe, LibraryError, OperationStatus, OtpMode, OtpSlotData, + Storage, VolumeMode, DEFAULT_ADMIN_PIN, DEFAULT_USER_PIN, }; use nitrokey_test::test as test_device; @@ -32,6 +32,33 @@ fn count_nitrokey_block_devices() -> usize { } #[test_device] +fn list_no_devices() { + let devices = nitrokey::list_devices(); + assert_ok!(Vec::<DeviceInfo>::new(), devices); +} + +#[test_device] +fn list_devices(_device: DeviceWrapper) { + let devices = unwrap_ok!(nitrokey::list_devices()); + for device in devices { + assert!(!device.path.is_empty()); + if let Some(model) = device.model { + match model { + 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); + } + nitrokey::Model::Storage => { + assert_eq!(None, device.serial_number); + } + } + } + } +} + +#[test_device] fn connect_no_device() { let mut manager = unwrap_ok!(nitrokey::take()); @@ -78,17 +105,74 @@ fn assert_empty_serial_number() { } #[test_device] +fn connect_path_no_device() { + let mut manager = unwrap_ok!(nitrokey::take()); + + assert_cmu_err!(CommunicationError::NotConnected, manager.connect_path("")); + assert_cmu_err!( + CommunicationError::NotConnected, + manager.connect_path("foobar") + ); + // TODO: add realistic path +} + +#[test_device] +fn connect_path(device: DeviceWrapper) { + let manager = device.into_manager(); + + assert_cmu_err!(CommunicationError::NotConnected, manager.connect_path("")); + assert_cmu_err!( + CommunicationError::NotConnected, + manager.connect_path("foobar") + ); + // TODO: add realistic path + + let devices = unwrap_ok!(nitrokey::list_devices()); + assert!(!devices.is_empty()); + for device in devices { + let connected_device = unwrap_ok!(manager.connect_path(device.path)); + assert_eq!(device.model, Some(connected_device.get_model())); + match device.model.unwrap() { + nitrokey::Model::Pro => { + assert!(device.serial_number.is_some()); + assert_ok!( + device.serial_number.unwrap(), + connected_device.get_serial_number() + ); + } + nitrokey::Model::Storage => { + assert_eq!(None, device.serial_number); + } + } + } +} + +#[test_device] fn disconnect(device: DeviceWrapper) { drop(device); assert_empty_serial_number(); } #[test_device] -fn get_serial_number(device: DeviceWrapper) { - let serial_number = unwrap_ok!(device.get_serial_number()); +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.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); +} + #[test_device] fn get_firmware_version(device: Pro) { let version = unwrap_ok!(device.get_firmware_version()); @@ -480,7 +564,7 @@ fn set_encrypted_volume_mode(device: Storage) { #[test_device] fn set_unencrypted_volume_mode(device: Storage) { fn assert_mode(device: &Storage, mode: VolumeMode) { - let status = unwrap_ok!(device.get_status()); + let status = unwrap_ok!(device.get_storage_status()); assert_eq!( status.unencrypted_volume.read_only, mode == VolumeMode::ReadOnly @@ -511,7 +595,7 @@ fn set_unencrypted_volume_mode(device: Storage) { #[test_device] fn get_storage_status(device: Storage) { - let status = unwrap_ok!(device.get_status()); + let status = unwrap_ok!(device.get_storage_status()); assert!(status.serial_number_sd_card > 0); assert!(status.serial_number_smart_card > 0); } @@ -531,7 +615,7 @@ fn get_production_info(device: Storage) { assert!(info.sd_card.oem != 0); assert!(info.sd_card.manufacturer != 0); - let status = unwrap_ok!(device.get_status()); + let status = unwrap_ok!(device.get_storage_status()); assert_eq!(status.firmware_version, info.firmware_version); assert_eq!(status.serial_number_sd_card, info.sd_card.serial_number); } @@ -546,16 +630,68 @@ fn clear_new_sd_card_warning(device: Storage) { // We have to perform an SD card operation to reset the new_sd_card_found field assert_ok!((), device.lock()); - let status = unwrap_ok!(device.get_status()); + let status = unwrap_ok!(device.get_storage_status()); assert!(status.new_sd_card_found); assert_ok!((), device.clear_new_sd_card_warning(DEFAULT_ADMIN_PIN)); - let status = unwrap_ok!(device.get_status()); + let status = unwrap_ok!(device.get_storage_status()); assert!(!status.new_sd_card_found); } #[test_device] +fn get_sd_card_usage(device: Storage) { + let range = unwrap_ok!(device.get_sd_card_usage()); + + assert!(range.end >= range.start); + assert!(range.end <= 100); +} + +#[test_device] +fn get_operation_status(device: Storage) { + assert_ok!(OperationStatus::Idle, device.get_operation_status()); +} + +#[test_device] +#[ignore] +fn fill_sd_card(device: Storage) { + // This test takes up to 60 min to execute and is therefore ignored by default. Use `cargo + // test -- --ignored fill_sd_card` to run the test. + + let mut device = device; + assert_ok!((), device.factory_reset(DEFAULT_ADMIN_PIN)); + thread::sleep(time::Duration::from_secs(3)); + assert_ok!((), device.build_aes_key(DEFAULT_ADMIN_PIN)); + + let status = unwrap_ok!(device.get_storage_status()); + assert!(!status.filled_with_random); + + assert_ok!((), device.fill_sd_card(DEFAULT_ADMIN_PIN)); + assert_cmd_err!(CommandError::WrongCrc, device.get_status()); + + let mut status = OperationStatus::Ongoing(0); + let mut last_progress = 0u8; + while status != OperationStatus::Idle { + status = unwrap_ok!(device.get_operation_status()); + if let OperationStatus::Ongoing(progress) = status { + assert!(progress <= 100, "progress = {}", progress); + assert!( + progress >= last_progress, + "progress = {}, last_progress = {}", + progress, + last_progress + ); + last_progress = progress; + + thread::sleep(time::Duration::from_secs(10)); + } + } + + let status = unwrap_ok!(device.get_storage_status()); + assert!(status.filled_with_random); +} + +#[test_device] fn export_firmware(device: Storage) { let mut device = device; assert_cmd_err!( |