diff options
| author | Robin Krahl <robin.krahl@ireas.org> | 2020-01-14 19:21:26 +0100 | 
|---|---|---|
| committer | Daniel Mueller <deso@posteo.net> | 2020-01-15 12:08:15 -0800 | 
| commit | 1e07212370806f3ea4ff7a6f66e5716b60ab1f77 (patch) | |
| tree | c562f40b9888b9daf62a9d9a42eeef8bfa35413f /nitrokey | |
| parent | 5251aa2d1665ee12245d5d5fe09c1f207272978f (diff) | |
| download | nitrocli-1e07212370806f3ea4ff7a6f66e5716b60ab1f77.tar.gz nitrocli-1e07212370806f3ea4ff7a6f66e5716b60ab1f77.tar.bz2 | |
Bump nitrokey dependency to version 0.5.1
This change updates the version of the nitrokey crate that we use to
0.5.1. As part of that, it replaces occurrences of Storage::get_status
with Storage::get_storage_status as the method has been renamed.
Import subrepo nitrokey/:nitrokey at 817409140a8778215d2d65d614d3672166fff576
Diffstat (limited to 'nitrokey')
| -rw-r--r-- | nitrokey/CHANGELOG.md | 23 | ||||
| -rw-r--r-- | nitrokey/Cargo.toml | 2 | ||||
| -rw-r--r-- | nitrokey/README.md | 42 | ||||
| -rw-r--r-- | nitrokey/TODO.md | 16 | ||||
| -rw-r--r-- | nitrokey/examples/list-devices.rs | 26 | ||||
| -rw-r--r-- | nitrokey/examples/otp.rs | 43 | ||||
| -rw-r--r-- | nitrokey/src/device/mod.rs | 238 | ||||
| -rw-r--r-- | nitrokey/src/device/pro.rs | 18 | ||||
| -rw-r--r-- | nitrokey/src/device/storage.rs | 161 | ||||
| -rw-r--r-- | nitrokey/src/device/wrapper.rs | 9 | ||||
| -rw-r--r-- | nitrokey/src/error.rs | 4 | ||||
| -rw-r--r-- | nitrokey/src/lib.rs | 136 | ||||
| -rw-r--r-- | nitrokey/tests/device.rs | 158 | 
13 files changed, 811 insertions, 65 deletions
| diff --git a/nitrokey/CHANGELOG.md b/nitrokey/CHANGELOG.md index d4451bc..cba0e83 100644 --- a/nitrokey/CHANGELOG.md +++ b/nitrokey/CHANGELOG.md @@ -3,6 +3,29 @@ Copyright (C) 2019-2020 Robin Krahl <robin.krahl@ireas.org>  SPDX-License-Identifier: CC0-1.0  --> +# v0.5.1 (2020-01-15) +- Fix serial number formatting for Nitrokey Pro devices with firmware 0.8 or +  older in the `list_devices` function. + +# 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/nitrokey/Cargo.toml b/nitrokey/Cargo.toml index b57591b..59af067 100644 --- a/nitrokey/Cargo.toml +++ b/nitrokey/Cargo.toml @@ -3,7 +3,7 @@  [package]  name = "nitrokey" -version = "0.4.0" +version = "0.5.1"  authors = ["Robin Krahl <robin.krahl@ireas.org>"]  edition = "2018"  homepage = "https://code.ireas.org/nitrokey-rs/" diff --git a/nitrokey/README.md b/nitrokey/README.md index 12a9f6d..9001193 100644 --- a/nitrokey/README.md +++ b/nitrokey/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://git.ireas.org/nitrokey-rs/tree/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/nitrokey/TODO.md b/nitrokey/TODO.md index 54525ef..92d4b04 100644 --- a/nitrokey/TODO.md +++ b/nitrokey/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/nitrokey/examples/list-devices.rs b/nitrokey/examples/list-devices.rs new file mode 100644 index 0000000..47fa054 --- /dev/null +++ b/nitrokey/examples/list-devices.rs @@ -0,0 +1,26 @@ +// Copyright (C) 2020 Robin Krahl <robin.krahl@ireas.org> +// SPDX-License-Identifier: CC0-1.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/nitrokey/examples/otp.rs b/nitrokey/examples/otp.rs new file mode 100644 index 0000000..f2c6f3c --- /dev/null +++ b/nitrokey/examples/otp.rs @@ -0,0 +1,43 @@ +// Copyright (C) 2020 Robin Krahl <robin.krahl@ireas.org> +// SPDX-License-Identifier: CC0-1.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/nitrokey/src/device/mod.rs b/nitrokey/src/device/mod.rs index 5e15f08..0234bf0 100644 --- a/nitrokey/src/device/mod.rs +++ b/nitrokey/src/device/mod.rs @@ -5,6 +5,8 @@ mod pro;  mod storage;  mod wrapper; +use std::convert::{TryFrom, TryInto}; +use std::ffi;  use std::fmt;  use libc; @@ -16,12 +18,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 +47,114 @@ 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, it uses the last +/// eight characters.  If these are all zero, the first eight characters are used instead.  This +/// function also makes sure that the returned string is lowercase, consistent with libnitrokey’s +/// hex string formatting. +/// +/// The reason for this behavior is that the Nitrokey Storage does not report its serial number at +/// all (all zero value), while the Nitrokey Pro with firmware 0.9 or later writes its serial +/// number to the last eight characters.  Nitrokey Pro devices with firmware 0.8 or earlier wrote +/// their serial number to the first eight characters. +fn get_hidapi_serial_number(serial_number: &str) -> Option<String> { +    let len = serial_number.len(); +    if len < 8 { +        // The serial number in the USB descriptor has 12 bytes, we need at least four of them +        return None; +    } + +    let iter = serial_number.char_indices().rev(); +    let first_non_null = iter.skip_while(|(_, c)| *c == '0').next(); +    if let Some((i, _)) = first_non_null { +        if len - i < 8 { +            // The last eight characters contain at least one non-zero character --> use them +            let mut serial_number = serial_number.split_at(len - 8).1.to_string(); +            serial_number.make_ascii_lowercase(); +            Some(serial_number) +        } else { +            // The last eight characters are all zero --> use the first eight +            let mut serial_number = serial_number.split_at(8).0.to_string(); +            serial_number.make_ascii_lowercase(); +            Some(serial_number) +        } +    } else { +        // The serial number is all zero +        None +    } +} +  /// A firmware version for a Nitrokey device.  #[derive(Clone, Copy, Debug, PartialEq)]  pub struct FirmwareVersion { @@ -58,6 +170,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 +244,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 +599,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 +616,53 @@ 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!(None, 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("00001234".to_string()), +            get_hidapi_serial_number("100000001234") +        ); +        assert_eq!( +            Some("12340000".to_string()), +            get_hidapi_serial_number("123400000000") +        ); +        assert_eq!( +            Some("00005678".to_string()), +            get_hidapi_serial_number("000000000000000000005678") +        ); +        assert_eq!( +            Some("00001234".to_string()), +            get_hidapi_serial_number("000012340000000000000000") +        ); +        assert_eq!( +            Some("0000ffff".to_string()), +            get_hidapi_serial_number("00000000000000000000FFFF") +        ); +        assert_eq!( +            Some("0000ffff".to_string()), +            get_hidapi_serial_number("00000000000000000000ffff") +        ); +    }  } diff --git a/nitrokey/src/device/pro.rs b/nitrokey/src/device/pro.rs index a65345e..591b730 100644 --- a/nitrokey/src/device/pro.rs +++ b/nitrokey/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/nitrokey/src/device/storage.rs b/nitrokey/src/device/storage.rs index 370ce36..deb2844 100644 --- a/nitrokey/src/device/storage.rs +++ b/nitrokey/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/nitrokey/src/device/wrapper.rs b/nitrokey/src/device/wrapper.rs index a3a18f9..69291ad 100644 --- a/nitrokey/src/device/wrapper.rs +++ b/nitrokey/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/nitrokey/src/error.rs b/nitrokey/src/error.rs index 9e6adc0..f9af594 100644 --- a/nitrokey/src/error.rs +++ b/nitrokey/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/nitrokey/src/lib.rs b/nitrokey/src/lib.rs index 059792d..9efad91 100644 --- a/nitrokey/src/lib.rs +++ b/nitrokey/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/nitrokey/tests/device.rs b/nitrokey/tests/device.rs index e367558..a88c956 100644 --- a/nitrokey/tests/device.rs +++ b/nitrokey/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!( | 
