aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md19
-rw-r--r--Cargo.toml2
-rw-r--r--README.md42
-rw-r--r--TODO.md16
-rw-r--r--examples/list-devices.rs26
-rw-r--r--examples/otp.rs43
-rw-r--r--src/device/mod.rs209
-rw-r--r--src/device/pro.rs18
-rw-r--r--src/device/storage.rs161
-rw-r--r--src/device/wrapper.rs9
-rw-r--r--src/error.rs4
-rw-r--r--src/lib.rs136
-rw-r--r--tests/device.rs158
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`.
diff --git a/Cargo.toml b/Cargo.toml
index b57591b..727d47c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -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/"
diff --git a/README.md b/README.md
index 12a9f6d..8ce093e 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/TODO.md b/TODO.md
index 54525ef..92d4b04 100644
--- a/TODO.md
+++ b/TODO.md
@@ -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),
}
}
diff --git a/src/lib.rs b/src/lib.rs
index 059792d..9efad91 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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!(